-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mnq): add a create-context nats custom command (#3655)
Co-authored-by: Jules Casteran <[email protected]> Co-authored-by: Rémy Léone <[email protected]>
- Loading branch information
1 parent
c39deda
commit 17055b4
Showing
26 changed files
with
1,491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
cmd/scw/testdata/test-all-usage-mnq-nats-create-context-usage.golden
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.