Skip to content

Commit

Permalink
Add iam role commands (#558)
Browse files Browse the repository at this point in the history
# Description

This PR adds IAM Role management commands:

- `exo iam role list`
- `exo iam role show`
- `exo iam role create` 
- `exo iam delete`
- `exo iam role update`
  • Loading branch information
kobajagi authored Nov 23, 2023
1 parent cb85f72 commit 4281338
Show file tree
Hide file tree
Showing 8 changed files with 642 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- iam: implement Org Policy management commands #553
- iam: implement Role management commands #558

### Improvements

Expand Down
14 changes: 14 additions & 0 deletions cmd/iam_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cmd

import (
"github.com/spf13/cobra"
)

var iamRoleCmd = &cobra.Command{
Use: "role",
Short: "IAM Role management",
}

func init() {
iamCmd.AddCommand(iamRoleCmd)
}
150 changes: 150 additions & 0 deletions cmd/iam_role_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"

"github.com/spf13/cobra"

"github.com/exoscale/cli/pkg/account"
"github.com/exoscale/cli/pkg/globalstate"
"github.com/exoscale/cli/pkg/output"
exoscale "github.com/exoscale/egoscale/v2"
exoapi "github.com/exoscale/egoscale/v2/api"
)

type iamRoleCreateCmd struct {
cliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"create"`

Name string `cli-arg:"#" cli-usage:"NAME"`
Description string `cli-flag:"description" cli-usage:"Role description"`
Permissions []string `cli-flag:"permissions" cli-usage:"Role permissions"`
Editable bool `cli-flag:"editable" cli-usage:"Set --editable=false do prevent editing Policy after creation"`
Labels map[string]string `cli-flag:"label" cli-usage:"Role labels (format: key=value)"`
Policy string `cli-flag:"policy" cli-usage:"Role policy (use '-' to read from STDIN)"`
}

func (c *iamRoleCreateCmd) cmdAliases() []string { return nil }

func (c *iamRoleCreateCmd) cmdShort() string {
return "Create IAM Role"
}

func (c *iamRoleCreateCmd) cmdLong() string {
return fmt.Sprintf(`This command creates a new IAM Role.
To read the Policy from STDIN, append '-' to the '--policy' flag.
Pro Tip: you can reuse an existing role policy by providing the output of the show command as input:
exo iam role show --policy --output-format json <role-name> | exo iam role create --name <new-role-name> --policy -
Supported output template annotations: %s`,
strings.Join(output.TemplateAnnotations(&iamRoleShowOutput{}), ", "))
}

func (c *iamRoleCreateCmd) cmdPreRun(cmd *cobra.Command, args []string) error {
return cliCommandDefaultPreRun(c, cmd, args)
}

func (c *iamRoleCreateCmd) cmdRun(cmd *cobra.Command, _ []string) error {
if c.Name == "" {
return errors.New("NAME not provided")
}

zone := account.CurrentAccount.DefaultZone
ctx := exoapi.WithEndpoint(
gContext,
exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone),
)

if c.Policy == "-" {
inputReader := cmd.InOrStdin()
b, err := io.ReadAll(inputReader)
if err != nil {
return fmt.Errorf("failed to read policy from stdin: %w", err)
}

c.Policy = string(b)
}

var obj iamPolicyOutput
err := json.Unmarshal([]byte(c.Policy), &obj)
if err != nil {
return fmt.Errorf("failed to parse policy: %w", err)
}

policy := &exoscale.IAMPolicy{
DefaultServiceStrategy: obj.DefaultServiceStrategy,
Services: map[string]exoscale.IAMPolicyService{},
}

if len(obj.Services) > 0 {
for name, sv := range obj.Services {
service := exoscale.IAMPolicyService{
Type: func() *string {
t := sv.Type
return &t
}(),
}

if len(sv.Rules) > 0 {
service.Rules = []exoscale.IAMPolicyServiceRule{}
for _, rl := range sv.Rules {

rule := exoscale.IAMPolicyServiceRule{
Action: func() *string {
t := rl.Action
return &t
}(),
}

if rl.Expression != "" {
rule.Expression = func() *string {
t := rl.Expression
return &t
}()
}

service.Rules = append(service.Rules, rule)
}
}

policy.Services[name] = service
}
}

role := &exoscale.IAMRole{
Name: &c.Name,
Description: &c.Description,
Editable: &c.Editable,
Labels: c.Labels,
Permissions: c.Permissions,
Policy: policy,
}

r, err := globalstate.EgoscaleClient.CreateIAMRole(ctx, zone, role)
if err != nil {
return err
}

if !globalstate.Quiet {
return (&iamRoleShowCmd{
cliCommandSettings: c.cliCommandSettings,
Role: *r.ID,
}).cmdRun(nil, nil)
}

return nil
}

func init() {
cobra.CheckErr(registerCLICommand(iamRoleCmd, &iamRoleCreateCmd{
cliCommandSettings: defaultCLICmdSettings(),
Editable: true,
}))
}
86 changes: 86 additions & 0 deletions cmd/iam_role_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cmd

import (
"fmt"

"github.com/google/uuid"
"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 iamRoleDeleteCmd struct {
cliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"delete"`

Role string `cli-arg:"#" cli-usage:"ID|NAME"`

Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"`
}

func (c *iamRoleDeleteCmd) cmdAliases() []string { return gDeleteAlias }

func (c *iamRoleDeleteCmd) cmdShort() string {
return "Delete IAM Role"
}

func (c *iamRoleDeleteCmd) cmdLong() string {
return `This command deletes an existing IAM Role.
It will fail if the Role is attached to an IAM Key.`
}

func (c *iamRoleDeleteCmd) cmdPreRun(cmd *cobra.Command, args []string) error {
return cliCommandDefaultPreRun(c, cmd, args)
}

func (c *iamRoleDeleteCmd) cmdRun(_ *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
}

found := false
for _, role := range roles {
if role.Name != nil && *role.Name == c.Role {
c.Role = *role.ID
found = true
break
}
}

if !found {
return fmt.Errorf("role with name %q not found", c.Role)
}
}

if !c.Force {
if !askQuestion(fmt.Sprintf("Are you sure you want to delete IAM Role %s?", c.Role)) {
return nil
}
}

var err error
decorateAsyncOperation(fmt.Sprintf("Deleting IAM role %s...", c.Role), func() {

err = globalstate.EgoscaleClient.DeleteIAMRole(ctx, zone, &egoscale.IAMRole{ID: &c.Role})
})
if err != nil {
return err
}

return nil
}

func init() {
cobra.CheckErr(registerCLICommand(iamRoleCmd, &iamRoleDeleteCmd{
cliCommandSettings: defaultCLICmdSettings(),
}))
}
76 changes: 76 additions & 0 deletions cmd/iam_role_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"fmt"
"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/utils"
exoapi "github.com/exoscale/egoscale/v2/api"
)

type iamRoleListItemOutput struct {
ID string `json:"key"`
Name string `json:"name"`
Editable bool `json:"type"`
}

type iamRoleListOutput []iamRoleListItemOutput

func (o *iamRoleListOutput) ToJSON() { output.JSON(o) }
func (o *iamRoleListOutput) ToText() { output.Text(o) }
func (o *iamRoleListOutput) ToTable() { output.Table(o) }

type iamRoleListCmd struct {
cliCommandSettings `cli-cmd:"-"`

_ bool `cli-cmd:"list"`
}

func (c *iamRoleListCmd) cmdAliases() []string { return gListAlias }

func (c *iamRoleListCmd) cmdShort() string { return "List IAM Roles" }

func (c *iamRoleListCmd) cmdLong() string {
return fmt.Sprintf(`This command lists existing IAM Roles.
Supported output template annotations: %s`,
strings.Join(output.TemplateAnnotations(&iamRoleListOutput{}), ", "))
}

func (c *iamRoleListCmd) cmdPreRun(cmd *cobra.Command, args []string) error {
return cliCommandDefaultPreRun(c, cmd, args)
}

func (c *iamRoleListCmd) cmdRun(_ *cobra.Command, _ []string) error {
zone := account.CurrentAccount.DefaultZone

ctx := exoapi.WithEndpoint(gContext, exoapi.NewReqEndpoint(account.CurrentAccount.Environment, zone))

iamRoles, err := globalstate.EgoscaleClient.ListIAMRoles(ctx, zone)
if err != nil {
return err
}

out := make(iamRoleListOutput, 0)

for _, role := range iamRoles {
out = append(out, iamRoleListItemOutput{
ID: utils.DefaultString(role.ID, ""),
Name: utils.DefaultString(role.Name, ""),
Editable: utils.DefaultBool(role.Editable, false),
})
}

return c.outputFunc(&out, err)
}

func init() {
cobra.CheckErr(registerCLICommand(iamRoleCmd, &iamRoleListCmd{
cliCommandSettings: defaultCLICmdSettings(),
}))
}
Loading

0 comments on commit 4281338

Please sign in to comment.