Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mnq): add a create-context nats custom command #3655

Merged
merged 2 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
jremy42 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading