Skip to content

Commit 17055b4

Browse files
jremy42Codelaxremyleone
authored
feat(mnq): add a create-context nats custom command (#3655)
Co-authored-by: Jules Casteran <jcasteran@scaleway.com> Co-authored-by: Rémy Léone <rleone@scaleway.com>
1 parent c39deda commit 17055b4

26 files changed

+1491
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ node_modules
66
# Local release artifacts
77
dist/
88
scw-cli-v2-version
9+
10+
# To avoid having differences in case you use a different shell than bash while recording the goldens
11+
docs/commands/autocomplete.md
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
This command help you configure your nats cli
4+
Contexts should are stored in $HOME/.config/nats/context
5+
Credentials and context file are saved in your nats context folder with 0600 permissions
6+
7+
USAGE:
8+
scw mnq nats create-context [arg=value ...]
9+
10+
EXAMPLES:
11+
Create a context in your nats server
12+
scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par
13+
14+
ARGS:
15+
[nats-account-id] ID of the NATS account
16+
[name] Name of the saved context, defaults to account name
17+
[credentials-name] Name of the created credentials
18+
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par)
19+
20+
FLAGS:
21+
-h, --help help for create-context
22+
23+
GLOBAL FLAGS:
24+
-c, --config string The path to the config file
25+
-D, --debug Enable debug mode
26+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
27+
-p, --profile string The config profile to use

cmd/scw/testdata/test-all-usage-mnq-nats-usage.golden

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ AVAILABLE COMMANDS:
1616
list-credentials List NATS credentials
1717
update-account Update the name of a NATS account
1818

19+
WORKFLOW COMMANDS:
20+
create-context Create a new context for natscli
21+
1922
FLAGS:
2023
-h, --help help for nats
2124

docs/commands/mnq.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Messaging and Queuing APIs.
44

55
- [MnQ NATS commands](#mnq-nats-commands)
66
- [Create a NATS account](#create-a-nats-account)
7+
- [Create a new context for natscli](#create-a-new-context-for-natscli)
78
- [Create NATS credentials](#create-nats-credentials)
89
- [Delete a NATS account](#delete-a-nats-account)
910
- [Delete NATS credentials](#delete-nats-credentials)
@@ -58,6 +59,40 @@ scw mnq nats create-account [arg=value ...]
5859

5960

6061

62+
### Create a new context for natscli
63+
64+
This command help you configure your nats cli
65+
Contexts should are stored in $HOME/.config/nats/context
66+
Credentials and context file are saved in your nats context folder with 0600 permissions
67+
68+
**Usage:**
69+
70+
```
71+
scw mnq nats create-context [arg=value ...]
72+
```
73+
74+
75+
**Args:**
76+
77+
| Name | | Description |
78+
|------|---|-------------|
79+
| nats-account-id | | ID of the NATS account |
80+
| name | | Name of the saved context, defaults to account name |
81+
| credentials-name | | Name of the created credentials |
82+
| region | Default: `fr-par`<br />One of: `fr-par` | Region to target. If none is passed will use default region from the config |
83+
84+
85+
**Examples:**
86+
87+
88+
Create a context in your nats server
89+
```
90+
scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par
91+
```
92+
93+
94+
95+
6196
### Create NATS credentials
6297

6398
Create a set of credentials for a NATS account, specified by its NATS account ID.

internal/namespaces/mnq/v1beta1/custom.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ func GetCommands() *core.Commands {
1111

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

14+
cmds.Merge(core.NewCommands(
15+
createContextCommand(),
16+
))
17+
1418
return cmds
1519
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package mnq
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/scaleway/scaleway-cli/v2/internal/core"
9+
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
10+
"github.com/scaleway/scaleway-sdk-go/scw"
11+
)
12+
13+
type natsContext struct {
14+
Description string `json:"description"`
15+
URL string `json:"url"`
16+
17+
// CredentialsPath is a path to file containing credentials
18+
CredentialsPath string `json:"creds"`
19+
}
20+
21+
type CreateContextRequest struct {
22+
NatsAccountID string
23+
ContextName string
24+
CredentialsName string
25+
Region scw.Region
26+
}
27+
28+
func createContextCommand() *core.Command {
29+
return &core.Command{
30+
Short: "Create a new context for natscli",
31+
Namespace: "mnq",
32+
Resource: "nats",
33+
Verb: "create-context",
34+
Groups: []string{"workflow"},
35+
Long: `This command help you configure your nats cli
36+
Contexts should are stored in $HOME/.config/nats/context
37+
Credentials and context file are saved in your nats context folder with 0600 permissions`,
38+
Examples: []*core.Example{
39+
{
40+
Short: "Create a context in your nats server",
41+
Raw: `scw mnq nats create-context <nats-account-id> credentials-name=<credential-name> region=fr-par`,
42+
},
43+
},
44+
ArgSpecs: core.ArgSpecs{
45+
{
46+
Name: "nats-account-id",
47+
Short: "ID of the NATS account",
48+
},
49+
{
50+
Name: "name",
51+
Short: "Name of the saved context, defaults to account name",
52+
},
53+
{
54+
Name: "credentials-name",
55+
Short: "Name of the created credentials",
56+
},
57+
core.RegionArgSpec((*mnq.NatsAPI)(nil).Regions()...),
58+
},
59+
ArgsType: reflect.TypeOf(CreateContextRequest{}),
60+
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
61+
args := argsI.(*CreateContextRequest)
62+
api := mnq.NewNatsAPI(core.ExtractClient(ctx))
63+
natsAccount, err := getNatsAccountID(ctx, args, api)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
var credentialsName string
69+
if args.CredentialsName != "" {
70+
credentialsName = args.CredentialsName
71+
} else {
72+
credentialsName = natsAccount.Name + core.GetRandomName("creds")
73+
}
74+
credentials, err := api.CreateNatsCredentials(&mnq.NatsAPICreateNatsCredentialsRequest{
75+
Region: args.Region,
76+
NatsAccountID: natsAccount.ID,
77+
Name: credentialsName,
78+
}, scw.WithContext(ctx))
79+
if err != nil {
80+
return nil, err
81+
}
82+
contextPath, err := saveNATSCredentials(ctx, credentials, natsAccount)
83+
if err != nil {
84+
return nil, err
85+
}
86+
return &core.SuccessResult{
87+
Message: "Nats context successfully created",
88+
Details: fmt.Sprintf("%s nats credentials was created\nSelect context using `nats context select %s`", credentials.Name, natsAccount.Name),
89+
Resource: contextPath,
90+
}, nil
91+
},
92+
}
93+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package mnq
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/scaleway/scaleway-cli/v2/internal/core"
11+
"github.com/scaleway/scaleway-cli/v2/internal/interactive"
12+
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
13+
"github.com/scaleway/scaleway-sdk-go/scw"
14+
)
15+
16+
type NatsEntity struct {
17+
Name string
18+
Content []byte
19+
}
20+
21+
func makeDirectoryIfNotExists(path string) error {
22+
if _, err := os.Stat(path); os.IsNotExist(err) {
23+
return os.MkdirAll(path, os.ModeDir|0755)
24+
}
25+
return nil
26+
}
27+
28+
func wrapError(err error, message, name, path string) error {
29+
return &core.CliError{
30+
Err: err,
31+
Message: fmt.Sprintf("%s into file %q", message, path),
32+
Details: fmt.Sprintf("You may want to delete created credentials %q", name),
33+
Code: 1,
34+
}
35+
}
36+
37+
func fileExists(filePath string) bool {
38+
_, err := os.Stat(filePath)
39+
return !os.IsNotExist(err)
40+
}
41+
42+
func natsContextFrom(account *mnq.NatsAccount, credsPath string) []byte {
43+
ctx := &natsContext{
44+
Description: "Nats context created by Scaleway CLI",
45+
URL: account.Endpoint,
46+
CredentialsPath: credsPath,
47+
}
48+
b, _ := json.Marshal(ctx)
49+
return b
50+
}
51+
52+
func writeFile(ctx context.Context, dir string, entity *NatsEntity, extension string) (string, error) {
53+
path := filepath.Join(dir, entity.Name+"."+extension)
54+
if err := makeDirectoryIfNotExists(dir); err != nil {
55+
return "", wrapError(err, "Failed to create directory", entity.Name, path)
56+
}
57+
if fileExists(path) {
58+
overWrite, err := promptOverWriteFile(ctx, path)
59+
if err != nil {
60+
return "", wrapError(err, "Failed to prompt for overwrite", entity.Name, path)
61+
}
62+
if !overWrite {
63+
return "", wrapError(nil, "File already exists", entity.Name, path)
64+
}
65+
}
66+
if err := os.WriteFile(path, entity.Content, 0600); err != nil {
67+
return "", wrapError(err, "Failed to write file", entity.Name, path)
68+
}
69+
_, _ = interactive.Println(entity.Name + " file has been successfully written to " + path)
70+
return path, nil
71+
}
72+
73+
func getNATSContextDir(ctx context.Context) (string, error) {
74+
xdgConfigHome := core.ExtractEnv(ctx, "XDG_CONFIG_HOME")
75+
interactive.Println("xdgConfigHome:", xdgConfigHome)
76+
if xdgConfigHome == "" {
77+
homeDir := core.ExtractEnv(ctx, "HOME")
78+
if homeDir == "" {
79+
return "", fmt.Errorf("both XDG_CONFIG_HOME and HOME are not set")
80+
}
81+
return filepath.Join(homeDir, ".config", "nats", "context"), nil
82+
}
83+
return xdgConfigHome, nil
84+
}
85+
86+
func saveNATSCredentials(ctx context.Context, creds *mnq.NatsCredentials, natsAccount *mnq.NatsAccount) (string, error) {
87+
natsContextDir, err := getNATSContextDir(ctx)
88+
if err != nil {
89+
return "", err
90+
}
91+
credsEntity := &NatsEntity{
92+
Name: creds.Name,
93+
Content: []byte(creds.Credentials.Content),
94+
}
95+
credsPath, err := writeFile(ctx, natsContextDir, credsEntity, "creds")
96+
if err != nil {
97+
return "", err
98+
}
99+
100+
contextEntity := &NatsEntity{
101+
Name: natsAccount.Name,
102+
Content: natsContextFrom(natsAccount, credsPath),
103+
}
104+
105+
contextPath, err := writeFile(ctx, natsContextDir, contextEntity, "json")
106+
if err != nil {
107+
return "", err
108+
}
109+
return contextPath, nil
110+
}
111+
112+
func getNatsAccountID(ctx context.Context, args *CreateContextRequest, api *mnq.NatsAPI) (*mnq.NatsAccount, error) {
113+
var natsAccount *mnq.NatsAccount
114+
if args.NatsAccountID == "" {
115+
natsAccountsResp, err := api.ListNatsAccounts(&mnq.NatsAPIListNatsAccountsRequest{
116+
Region: args.Region,
117+
})
118+
if err != nil {
119+
return nil, fmt.Errorf("failed to list nats account: %w", err)
120+
}
121+
natsAccount, err = promptNatsAccounts(ctx, natsAccountsResp.NatsAccounts, natsAccountsResp.TotalCount)
122+
if err != nil {
123+
return nil, fmt.Errorf("failed to list nats account: %w", err)
124+
}
125+
} else {
126+
var err error
127+
natsAccount, err = api.GetNatsAccount(&mnq.NatsAPIGetNatsAccountRequest{
128+
Region: args.Region,
129+
NatsAccountID: args.NatsAccountID,
130+
}, scw.WithContext(ctx))
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to get nats account: %w", err)
133+
}
134+
}
135+
return natsAccount, nil
136+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package mnq
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/scaleway/scaleway-cli/v2/internal/interactive"
8+
mnq "github.com/scaleway/scaleway-sdk-go/api/mnq/v1beta1"
9+
)
10+
11+
func promptNatsAccounts(ctx context.Context, natsAccounts []*mnq.NatsAccount, totalCount uint64) (*mnq.NatsAccount, error) {
12+
if totalCount == 0 {
13+
return nil, fmt.Errorf("no nats account found, please create a NATS account with 'scw mnq nats create-account'")
14+
}
15+
16+
if !interactive.IsInteractive {
17+
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")
18+
}
19+
if totalCount == 1 {
20+
return natsAccounts[0], nil
21+
}
22+
23+
defaultIndex := 0
24+
natsAccountsName := make([]string, len(natsAccounts))
25+
for i := range natsAccounts {
26+
natsAccountsName[i] = fmt.Sprintf("%s %s", natsAccounts[i].Name, natsAccounts[i].Region)
27+
}
28+
prompt := interactive.ListPrompt{
29+
Prompt: "Choose your nats account",
30+
Choices: natsAccountsName,
31+
DefaultIndex: defaultIndex,
32+
}
33+
_, _ = interactive.Println()
34+
index, err := prompt.Execute(ctx)
35+
if err != nil {
36+
return nil, err
37+
}
38+
return natsAccounts[index], nil
39+
}
40+
41+
func promptOverWriteFile(ctx context.Context, filePath string) (bool, error) {
42+
if !interactive.IsInteractive {
43+
return false, fmt.Errorf("file Exist")
44+
}
45+
46+
config := interactive.PromptBoolConfig{
47+
Ctx: ctx,
48+
Prompt: "The file " + filePath + " already exists. Do you want to overwrite it?",
49+
DefaultValue: true,
50+
}
51+
overWrite, _ := interactive.PromptBoolWithConfig(&config)
52+
return overWrite, nil
53+
}

0 commit comments

Comments
 (0)