diff --git a/CHANGELOG.md b/CHANGELOG.md index 49f83de5..03b9ad94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## 1.74.5 +## Unreleased ### Features - iam: implement Org Policy management commands #553 - iam: implement Role management commands #558 +- iam: implement API Key management commands #560 + +## 1.74.5 ### Improvements diff --git a/cmd/iam_api_key.go b/cmd/iam_api_key.go new file mode 100644 index 00000000..ec705fd9 --- /dev/null +++ b/cmd/iam_api_key.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var iamAPIKeyCmd = &cobra.Command{ + Use: "api-key", + Short: "API Key management", +} + +func init() { + iamCmd.AddCommand(iamAPIKeyCmd) +} diff --git a/cmd/iam_api_key_create.go b/cmd/iam_api_key_create.go new file mode 100644 index 00000000..17b556d9 --- /dev/null +++ b/cmd/iam_api_key_create.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + "github.com/exoscale/cli/pkg/account" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/utils" + exoscale "github.com/exoscale/egoscale/v2" + exoapi "github.com/exoscale/egoscale/v2/api" +) + +type iamAPIKeyShowOutput struct { + Name string `json:"name"` + Key string `json:"key"` + Secret string `json:"secret"` + Role string `json:"role-id"` +} + +func (o *iamAPIKeyShowOutput) ToJSON() { output.JSON(o) } +func (o *iamAPIKeyShowOutput) ToText() { output.Text(o) } +func (o *iamAPIKeyShowOutput) ToTable() { output.Table(o) } + +type iamAPIKeyCreateCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"create"` + + Name string `cli-arg:"#" cli-usage:"NAME"` + Role string `cli-arg:"#" cli-usage:"ROLE-NAME|ROLE-ID"` +} + +func (c *iamAPIKeyCreateCmd) cmdAliases() []string { return nil } + +func (c *iamAPIKeyCreateCmd) cmdShort() string { + return "Create API Key" +} + +func (c *iamAPIKeyCreateCmd) cmdLong() string { + return fmt.Sprintf(`This command creates a new API Key. +Because Secret is only printed during API Key creation, --quiet (-Q) flag is not implemented for this command. + +Supported output template annotations: %s`, + strings.Join(output.TemplateAnnotations(&iamAPIKeyShowOutput{}), ", ")) +} + +func (c *iamAPIKeyCreateCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) +} + +func (c *iamAPIKeyCreateCmd) cmdRun(cmd *cobra.Command, _ []string) error { + zone := account.CurrentAccount.DefaultZone + ctx := exoapi.WithEndpoint( + gContext, + exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone), + ) + + if _, err := uuid.Parse(c.Role); err != nil { + roles, err := globalstate.EgoscaleClient.ListIAMRoles(ctx, zone) + if err != nil { + return err + } + + for _, role := range roles { + if role.Name != nil && *role.Name == c.Role { + c.Role = *role.ID + break + } + } + } + + role, err := globalstate.EgoscaleClient.GetIAMRole(ctx, zone, c.Role) + if err != nil { + return err + } + + apikey := &exoscale.APIKey{ + Name: &c.Name, + RoleID: role.ID, + } + + k, secret, err := globalstate.EgoscaleClient.CreateAPIKey(ctx, zone, apikey) + if err != nil { + return err + } + + out := iamAPIKeyShowOutput{ + Name: utils.DefaultString(k.Name, ""), + Key: utils.DefaultString(k.Key, ""), + Secret: secret, + Role: utils.DefaultString(k.RoleID, ""), + } + + return c.outputFunc(&out, nil) +} + +func init() { + cobra.CheckErr(registerCLICommand(iamAPIKeyCmd, &iamAPIKeyCreateCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/iam_api_key_delete.go b/cmd/iam_api_key_delete.go new file mode 100644 index 00000000..88f6c76c --- /dev/null +++ b/cmd/iam_api_key_delete.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/exoscale/cli/pkg/account" + "github.com/exoscale/cli/pkg/globalstate" + egoscale "github.com/exoscale/egoscale/v2" + exoapi "github.com/exoscale/egoscale/v2/api" +) + +type iamAPIKeyDeleteCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"delete"` + + APIKey string `cli-arg:"#" cli-usage:"NAME|KEY"` + + Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` +} + +func (c *iamAPIKeyDeleteCmd) cmdAliases() []string { return gDeleteAlias } + +func (c *iamAPIKeyDeleteCmd) cmdShort() string { + return "Delete an API Key" +} + +func (c *iamAPIKeyDeleteCmd) cmdLong() string { + return `This command deletes existing API Key.` +} + +func (c *iamAPIKeyDeleteCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) +} + +func (c *iamAPIKeyDeleteCmd) cmdRun(_ *cobra.Command, _ []string) error { + zone := account.CurrentAccount.DefaultZone + ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone)) + + if len(c.APIKey) == 27 && strings.HasPrefix(c.APIKey, "EX") { + _, err := globalstate.EgoscaleClient.GetAPIKey(ctx, zone, c.APIKey) + if err != nil { + return err + } + } else { + apikeys, err := globalstate.EgoscaleClient.ListAPIKeys(ctx, zone) + if err != nil { + return err + } + + found := false + for _, apikey := range apikeys { + if apikey.Name != nil && *apikey.Name == c.APIKey { + c.APIKey = *apikey.Key + found = true + break + } + } + + if !found { + return fmt.Errorf("key with name %q not found", c.APIKey) + } + } + + if !c.Force { + if !askQuestion(fmt.Sprintf("Are you sure you want to delete API Key %q?", c.APIKey)) { + return nil + } + } + + var err error + decorateAsyncOperation(fmt.Sprintf("Deleting API Key %s...", c.APIKey), func() { + err = globalstate.EgoscaleClient.DeleteAPIKey(ctx, zone, &egoscale.APIKey{Key: &c.APIKey}) + }) + if err != nil { + return err + } + + return nil +} + +func init() { + cobra.CheckErr(registerCLICommand(iamAPIKeyCmd, &iamAPIKeyDeleteCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/iam_api_key_list.go b/cmd/iam_api_key_list.go new file mode 100644 index 00000000..d514ed03 --- /dev/null +++ b/cmd/iam_api_key_list.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/exoscale/cli/pkg/account" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + "github.com/exoscale/cli/utils" + exoapi "github.com/exoscale/egoscale/v2/api" +) + +type iamAPIKeyListItemOutput struct { + Name string `json:"name"` + Key string `json:"key"` + Role string `json:"role-id"` +} + +type iamAPIKeyListOutput []iamAPIKeyListItemOutput + +func (o *iamAPIKeyListOutput) ToJSON() { output.JSON(o) } +func (o *iamAPIKeyListOutput) ToText() { output.Text(o) } +func (o *iamAPIKeyListOutput) ToTable() { + t := table.NewTable(os.Stdout) + + t.SetHeader([]string{ + "NAME", + "KEY", + "ROLE", + }) + defer t.Render() + + zone := account.CurrentAccount.DefaultZone + ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone)) + + // For better UX we will print both role name and ID + rolesMap := map[string]string{} + iamRoles, err := globalstate.EgoscaleClient.ListIAMRoles(ctx, zone) + // If API returns error, can continue (print name only) as this is non-essential feature + if err == nil { + for _, role := range iamRoles { + if role.ID != nil && role.Name != nil { + rolesMap[*role.ID] = *role.Name + } + } + } + + for _, apikey := range *o { + role := apikey.Role + if name, ok := rolesMap[apikey.Role]; ok { + role = fmt.Sprintf("%s (%s)", name, apikey.Role) + } + + t.Append([]string{ + apikey.Name, + apikey.Key, + role, + }) + } +} + +type iamAPIKeyListCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"list"` +} + +func (c *iamAPIKeyListCmd) cmdAliases() []string { return gListAlias } + +func (c *iamAPIKeyListCmd) cmdShort() string { return "List API Keys" } + +func (c *iamAPIKeyListCmd) cmdLong() string { + return fmt.Sprintf(`This command lists all API Keys. + +Supported output template annotations: %s`, + strings.Join(output.TemplateAnnotations(&iamAPIKeyListOutput{}), ", ")) +} + +func (c *iamAPIKeyListCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) +} + +func (c *iamAPIKeyListCmd) cmdRun(_ *cobra.Command, _ []string) error { + zone := account.CurrentAccount.DefaultZone + + ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone)) + + apikeys, err := globalstate.EgoscaleClient.ListAPIKeys(ctx, zone) + if err != nil { + return err + } + + out := make(iamAPIKeyListOutput, 0) + + for _, apikey := range apikeys { + out = append(out, iamAPIKeyListItemOutput{ + Name: utils.DefaultString(apikey.Name, ""), + Key: utils.DefaultString(apikey.Key, ""), + Role: utils.DefaultString(apikey.RoleID, ""), + }) + } + + return c.outputFunc(&out, err) +} + +func init() { + cobra.CheckErr(registerCLICommand(iamAPIKeyCmd, &iamAPIKeyListCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +}