Skip to content

Commit

Permalink
feat(mnq): add a create-context nats custom command (#3655)
Browse files Browse the repository at this point in the history
Co-authored-by: Jules Casteran <[email protected]>
Co-authored-by: Rémy Léone <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2024
1 parent c39deda commit 17055b4
Show file tree
Hide file tree
Showing 26 changed files with 1,491 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ node_modules
# Local release artifacts
dist/
scw-cli-v2-version

# To avoid having differences in case you use a different shell than bash while recording the goldens
docs/commands/autocomplete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
This command help you configure your nats cli
Contexts should are stored in $HOME/.config/nats/context
Credentials and context file are saved in your nats context folder with 0600 permissions

USAGE:
scw mnq nats create-context [arg=value ...]

EXAMPLES:
Create a context in your nats server
scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par

ARGS:
[nats-account-id] ID of the NATS account
[name] Name of the saved context, defaults to account name
[credentials-name] Name of the created credentials
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par)

FLAGS:
-h, --help help for create-context

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
3 changes: 3 additions & 0 deletions cmd/scw/testdata/test-all-usage-mnq-nats-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ AVAILABLE COMMANDS:
list-credentials List NATS credentials
update-account Update the name of a NATS account

WORKFLOW COMMANDS:
create-context Create a new context for natscli

FLAGS:
-h, --help help for nats

Expand Down
35 changes: 35 additions & 0 deletions docs/commands/mnq.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Messaging and Queuing APIs.

- [MnQ NATS commands](#mnq-nats-commands)
- [Create a NATS account](#create-a-nats-account)
- [Create a new context for natscli](#create-a-new-context-for-natscli)
- [Create NATS credentials](#create-nats-credentials)
- [Delete a NATS account](#delete-a-nats-account)
- [Delete NATS credentials](#delete-nats-credentials)
Expand Down Expand Up @@ -58,6 +59,40 @@ scw mnq nats create-account [arg=value ...]



### Create a new context for natscli

This command help you configure your nats cli
Contexts should are stored in $HOME/.config/nats/context
Credentials and context file are saved in your nats context folder with 0600 permissions

**Usage:**

```
scw mnq nats create-context [arg=value ...]
```


**Args:**

| Name | | Description |
|------|---|-------------|
| nats-account-id | | ID of the NATS account |
| name | | Name of the saved context, defaults to account name |
| credentials-name | | Name of the created credentials |
| region | Default: `fr-par`<br />One of: `fr-par` | Region to target. If none is passed will use default region from the config |


**Examples:**


Create a context in your nats server
```
scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par
```




### Create NATS credentials

Create a set of credentials for a NATS account, specified by its NATS account ID.
Expand Down
4 changes: 4 additions & 0 deletions internal/namespaces/mnq/v1beta1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ func GetCommands() *core.Commands {

human.RegisterMarshalerFunc(mnq.SnsInfoStatus(""), human.EnumMarshalFunc(mnqSqsInfoStatusMarshalSpecs))

cmds.Merge(core.NewCommands(
createContextCommand(),
))

return cmds
}
93 changes: 93 additions & 0 deletions internal/namespaces/mnq/v1beta1/custom_nats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package mnq

import (
"context"
"fmt"
"reflect"

"github.com/scaleway/scaleway-cli/v2/internal/core"
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type natsContext struct {
Description string `json:"description"`
URL string `json:"url"`

// CredentialsPath is a path to file containing credentials
CredentialsPath string `json:"creds"`
}

type CreateContextRequest struct {
NatsAccountID string
ContextName string
CredentialsName string
Region scw.Region
}

func createContextCommand() *core.Command {
return &core.Command{
Short: "Create a new context for natscli",
Namespace: "mnq",
Resource: "nats",
Verb: "create-context",
Groups: []string{"workflow"},
Long: `This command help you configure your nats cli
Contexts should are stored in $HOME/.config/nats/context
Credentials and context file are saved in your nats context folder with 0600 permissions`,
Examples: []*core.Example{
{
Short: "Create a context in your nats server",
Raw: `scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par`,
},
},
ArgSpecs: core.ArgSpecs{
{
Name: "nats-account-id",
Short: "ID of the NATS account",
},
{
Name: "name",
Short: "Name of the saved context, defaults to account name",
},
{
Name: "credentials-name",
Short: "Name of the created credentials",
},
core.RegionArgSpec((*mnq.NatsAPI)(nil).Regions()...),
},
ArgsType: reflect.TypeOf(CreateContextRequest{}),
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
args := argsI.(*CreateContextRequest)
api := mnq.NewNatsAPI(core.ExtractClient(ctx))
natsAccount, err := getNatsAccountID(ctx, args, api)
if err != nil {
return nil, err
}

var credentialsName string
if args.CredentialsName != "" {
credentialsName = args.CredentialsName
} else {
credentialsName = natsAccount.Name + core.GetRandomName("creds")
}
credentials, err := api.CreateNatsCredentials(&mnq.NatsAPICreateNatsCredentialsRequest{
Region: args.Region,
NatsAccountID: natsAccount.ID,
Name: credentialsName,
}, scw.WithContext(ctx))
if err != nil {
return nil, err
}
contextPath, err := saveNATSCredentials(ctx, credentials, natsAccount)
if err != nil {
return nil, err
}
return &core.SuccessResult{
Message: "Nats context successfully created",
Details: fmt.Sprintf("%s nats credentials was created\nSelect context using `nats context select %s`", credentials.Name, natsAccount.Name),
Resource: contextPath,
}, nil
},
}
}
136 changes: 136 additions & 0 deletions internal/namespaces/mnq/v1beta1/custom_nats_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package mnq

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/scaleway/scaleway-cli/v2/internal/core"
"github.com/scaleway/scaleway-cli/v2/internal/interactive"
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type NatsEntity struct {
Name string
Content []byte
}

func makeDirectoryIfNotExists(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.MkdirAll(path, os.ModeDir|0755)
}
return nil
}

func wrapError(err error, message, name, path string) error {
return &core.CliError{
Err: err,
Message: fmt.Sprintf("%s into file %q", message, path),
Details: fmt.Sprintf("You may want to delete created credentials %q", name),
Code: 1,
}
}

func fileExists(filePath string) bool {
_, err := os.Stat(filePath)
return !os.IsNotExist(err)
}

func natsContextFrom(account *mnq.NatsAccount, credsPath string) []byte {
ctx := &natsContext{
Description: "Nats context created by Scaleway CLI",
URL: account.Endpoint,
CredentialsPath: credsPath,
}
b, _ := json.Marshal(ctx)
return b
}

func writeFile(ctx context.Context, dir string, entity *NatsEntity, extension string) (string, error) {
path := filepath.Join(dir, entity.Name+"."+extension)
if err := makeDirectoryIfNotExists(dir); err != nil {
return "", wrapError(err, "Failed to create directory", entity.Name, path)
}
if fileExists(path) {
overWrite, err := promptOverWriteFile(ctx, path)
if err != nil {
return "", wrapError(err, "Failed to prompt for overwrite", entity.Name, path)
}
if !overWrite {
return "", wrapError(nil, "File already exists", entity.Name, path)
}
}
if err := os.WriteFile(path, entity.Content, 0600); err != nil {
return "", wrapError(err, "Failed to write file", entity.Name, path)
}
_, _ = interactive.Println(entity.Name + " file has been successfully written to " + path)
return path, nil
}

func getNATSContextDir(ctx context.Context) (string, error) {
xdgConfigHome := core.ExtractEnv(ctx, "XDG_CONFIG_HOME")
interactive.Println("xdgConfigHome:", xdgConfigHome)
if xdgConfigHome == "" {
homeDir := core.ExtractEnv(ctx, "HOME")
if homeDir == "" {
return "", fmt.Errorf("both XDG_CONFIG_HOME and HOME are not set")
}
return filepath.Join(homeDir, ".config", "nats", "context"), nil
}
return xdgConfigHome, nil
}

func saveNATSCredentials(ctx context.Context, creds *mnq.NatsCredentials, natsAccount *mnq.NatsAccount) (string, error) {
natsContextDir, err := getNATSContextDir(ctx)
if err != nil {
return "", err
}
credsEntity := &NatsEntity{
Name: creds.Name,
Content: []byte(creds.Credentials.Content),
}
credsPath, err := writeFile(ctx, natsContextDir, credsEntity, "creds")
if err != nil {
return "", err
}

contextEntity := &NatsEntity{
Name: natsAccount.Name,
Content: natsContextFrom(natsAccount, credsPath),
}

contextPath, err := writeFile(ctx, natsContextDir, contextEntity, "json")
if err != nil {
return "", err
}
return contextPath, nil
}

func getNatsAccountID(ctx context.Context, args *CreateContextRequest, api *mnq.NatsAPI) (*mnq.NatsAccount, error) {
var natsAccount *mnq.NatsAccount
if args.NatsAccountID == "" {
natsAccountsResp, err := api.ListNatsAccounts(&mnq.NatsAPIListNatsAccountsRequest{
Region: args.Region,
})
if err != nil {
return nil, fmt.Errorf("failed to list nats account: %w", err)
}
natsAccount, err = promptNatsAccounts(ctx, natsAccountsResp.NatsAccounts, natsAccountsResp.TotalCount)
if err != nil {
return nil, fmt.Errorf("failed to list nats account: %w", err)
}
} else {
var err error
natsAccount, err = api.GetNatsAccount(&mnq.NatsAPIGetNatsAccountRequest{
Region: args.Region,
NatsAccountID: args.NatsAccountID,
}, scw.WithContext(ctx))
if err != nil {
return nil, fmt.Errorf("failed to get nats account: %w", err)
}
}
return natsAccount, nil
}
53 changes: 53 additions & 0 deletions internal/namespaces/mnq/v1beta1/custom_nats_prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package mnq

import (
"context"
"fmt"

"github.com/scaleway/scaleway-cli/v2/internal/interactive"
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
)

func promptNatsAccounts(ctx context.Context, natsAccounts []*mnq.NatsAccount, totalCount uint64) (*mnq.NatsAccount, error) {
if totalCount == 0 {
return nil, fmt.Errorf("no nats account found, please create a NATS account with 'scw mnq nats create-account'")
}

if !interactive.IsInteractive {
return nil, fmt.Errorf("failed to create NATS context: Multiple NATS accounts found. Please provide an account ID explicitly as the command is not running in interactive mode")
}
if totalCount == 1 {
return natsAccounts[0], nil
}

defaultIndex := 0
natsAccountsName := make([]string, len(natsAccounts))
for i := range natsAccounts {
natsAccountsName[i] = fmt.Sprintf("%s %s", natsAccounts[i].Name, natsAccounts[i].Region)
}
prompt := interactive.ListPrompt{
Prompt: "Choose your nats account",
Choices: natsAccountsName,
DefaultIndex: defaultIndex,
}
_, _ = interactive.Println()
index, err := prompt.Execute(ctx)
if err != nil {
return nil, err
}
return natsAccounts[index], nil
}

func promptOverWriteFile(ctx context.Context, filePath string) (bool, error) {
if !interactive.IsInteractive {
return false, fmt.Errorf("file Exist")
}

config := interactive.PromptBoolConfig{
Ctx: ctx,
Prompt: "The file " + filePath + " already exists. Do you want to overwrite it?",
DefaultValue: true,
}
overWrite, _ := interactive.PromptBoolWithConfig(&config)
return overWrite, nil
}
Loading

0 comments on commit 17055b4

Please sign in to comment.