Skip to content

Commit

Permalink
Update doctl to support Spaces Keys API
Browse files Browse the repository at this point in the history
  • Loading branch information
lee-aaron committed Jan 17, 2025
1 parent 6707369 commit 0994d94
Show file tree
Hide file tree
Showing 10 changed files with 794 additions and 0 deletions.
3 changes: 3 additions & 0 deletions commands/command_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/digitalocean/doctl/commands/displayers"
"github.com/digitalocean/doctl/do"
"github.com/digitalocean/doctl/internal/apps/builder"

"github.com/spf13/viper"
)

Expand Down Expand Up @@ -74,6 +75,7 @@ type CmdConfig struct {
Monitoring func() do.MonitoringService
Serverless func() do.ServerlessService
OAuth func() do.OAuthService
SpacesKeys func() do.SpacesKeysService
}

// NewCmdConfig creates an instance of a CmdConfig.
Expand Down Expand Up @@ -130,6 +132,7 @@ func NewCmdConfig(ns string, dc doctl.Config, out io.Writer, args []string, init
return do.NewServerlessService(godoClient, getServerlessDirectory(), accessToken)
}
c.OAuth = func() do.OAuthService { return do.NewOAuthService(godoClient) }
c.SpacesKeys = func() do.SpacesKeysService { return do.NewSpacesKeysService(godoClient) }

return nil
},
Expand Down
4 changes: 4 additions & 0 deletions commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/digitalocean/doctl/do"
domocks "github.com/digitalocean/doctl/do/mocks"
"github.com/digitalocean/doctl/internal/apps/builder"

"github.com/digitalocean/godo"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -254,6 +255,7 @@ type tcMocks struct {
appBuilder *builder.MockComponentBuilder
appDockerEngineClient *builder.MockDockerEngineClient
oauth *domocks.MockOAuthService
spacesKeys *domocks.MockSpacesKeysService
}

func withTestClient(t *testing.T, tFn testFn) {
Expand Down Expand Up @@ -303,6 +305,7 @@ func withTestClient(t *testing.T, tFn testFn) {
appBuilder: builder.NewMockComponentBuilder(ctrl),
appDockerEngineClient: builder.NewMockDockerEngineClient(ctrl),
oauth: domocks.NewMockOAuthService(ctrl),
spacesKeys: domocks.NewMockSpacesKeysService(ctrl),
}

testConfig := doctl.NewTestConfig()
Expand Down Expand Up @@ -360,6 +363,7 @@ func withTestClient(t *testing.T, tFn testFn) {
Monitoring: func() do.MonitoringService { return tm.monitoring },
Serverless: func() do.ServerlessService { return tm.serverless },
OAuth: func() do.OAuthService { return tm.oauth },
SpacesKeys: func() do.SpacesKeysService { return tm.spacesKeys },
}

tFn(config, tm)
Expand Down
72 changes: 72 additions & 0 deletions commands/displayers/spaces_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2018 The Doctl Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package displayers

import (
"io"

"github.com/digitalocean/doctl/do"
)

type SpacesKey struct {
SpacesKeys []do.SpacesKey
}

var _ Displayable = &SpacesKey{}

// ColMap implements Displayable.
func (s *SpacesKey) ColMap() map[string]string {
return map[string]string{
"Name": "Name",
"AccessKey": "Access Key",
"SecretKey": "Secret Key",
"Grants": "Grants",
"CreatedAt": "Created At",
}
}

// Cols implements Displayable.
func (s *SpacesKey) Cols() []string {
return []string{
"Name",
"AccessKey",
"SecretKey",
"Grants",
"CreatedAt",
}
}

// JSON implements Displayable.
func (s *SpacesKey) JSON(out io.Writer) error {
return writeJSON(s.SpacesKeys, out)
}

// KV implements Displayable.
func (s *SpacesKey) KV() []map[string]any {
out := make([]map[string]any, 0, len(s.SpacesKeys))

for _, key := range s.SpacesKeys {
m := map[string]any{
"Name": key.Name,
"AccessKey": key.AccessKey,
"SecretKey": key.SecretKey,
"Grants": key.GrantString(),
"CreatedAt": key.CreatedAt,
}

out = append(out, m)
}

return out
}
2 changes: 2 additions & 0 deletions commands/doit.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/digitalocean/doctl"

"github.com/fatih/color"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -187,6 +188,7 @@ func addCommands() {
DoitCmd.AddCommand(OneClicks())
DoitCmd.AddCommand(Monitoring())
DoitCmd.AddCommand(Serverless())
DoitCmd.AddCommand(Spaces())
}

func computeCmd() *Command {
Expand Down
32 changes: 32 additions & 0 deletions commands/spaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
Copyright 2018 The Doctl Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package commands

import "github.com/spf13/cobra"

func Spaces() *Command {
cmd := &Command{
Command: &cobra.Command{
Use: "spaces",
Aliases: []string{"sp"},
Short: "Display commands that manage DigitalOcean Spaces.",
Long: "The subcommands of `doctl spaces` allow you to access and manage Spaces.",
GroupID: manageResourcesGroup,
},
}

cmd.AddCommand(SpacesKeys())

return cmd
}
219 changes: 219 additions & 0 deletions commands/spaces_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
Copyright 2018 The Doctl Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package commands

import (
"fmt"
"strings"

"github.com/digitalocean/doctl"
"github.com/digitalocean/doctl/commands/displayers"
"github.com/digitalocean/doctl/do"

"github.com/digitalocean/godo"
"github.com/spf13/cobra"
)

// SpacesKeys creates a new command that groups the subcommands for managing DigitalOcean Spaces Keys.
func SpacesKeys() *Command {
cmd := &Command{
Command: &cobra.Command{
Use: "keys",
Aliases: []string{"k"},
Short: "Display commands that manage DigitalOcean Spaces Keys.",
Long: "The subcommands of `doctl spaces keys` allow you to access and manage Spaces Keys.",
},
}

createSpacesKeyDesc := "Create a key for a Space with the provided name."
cmdSpacesKeysCreate := CmdBuilder(cmd, spacesKeysCreate, "create <name>", "Create a key for a Space.", createSpacesKeyDesc, Writer)
AddStringSliceFlag(cmdSpacesKeysCreate, "grants", "g", []string{},
`A list of grants to add to the key. The permission should be either 'read', 'readwrite', or 'fullaccess'.
Format: `+"`"+`"bucket=your-bucket;permission=your-permission"`+"`", requiredOpt())
cmdSpacesKeysCreate.Example = "doctl spaces keys create my-key --grants 'bucket=my-bucket;permission=readwrite'"

listSpacesKeysDesc := "List all keys for a Space."
cmdSpacesKeysList := CmdBuilder(
cmd,
spacesKeysList,
"list",
"List all keys for a Space.",
listSpacesKeysDesc,
Writer, aliasOpt("ls"), displayerType(&displayers.SpacesKey{}),
)
cmdSpacesKeysList.Example = "The following command lists all Spaces Keys and uses the `--format` flag to return only the Name and Grants of each key. `doctl spaces keys list --format Name,Grants`"

deleteSpacesKeyDesc := "Delete a key for a Space."
cmdSpacesKeysDelete := CmdBuilder(
cmd,
spacesKeysDelete,
"delete <key>",
"Delete a key for a Space.",
deleteSpacesKeyDesc,
Writer, aliasOpt("rm"),
)
cmdSpacesKeysDelete.Example = "doctl spaces keys delete DOACCESSKEY"

updateSpacesKeyDesc := "Update a key for a Space."
cmdSpacesKeysUpdate := CmdBuilder(cmd, spacesKeysUpdate, "update <key>", "Update a key for a Space.", updateSpacesKeyDesc, Writer)
AddStringFlag(cmdSpacesKeysUpdate, "name", "n", "", "The new name for the key.", requiredOpt())
AddStringSliceFlag(cmdSpacesKeysUpdate, "grants", "g", []string{},
`A list of grants to set to the key. The permission should be either 'read', 'readwrite', or 'fullaccess'.
Format: `+"`"+`"bucket=your-bucket;permission=your-permission"`+"`", requiredOpt())
cmdSpacesKeysUpdate.Example = "doctl spaces keys update DOACCESSKEY --name new-key --grants 'bucket=my-bucket;permission=readwrite'"

return cmd
}

func spacesKeysCreate(c *CmdConfig) error {
err := ensureOneArg(c)
if err != nil {
return err
}

grants, err := c.Doit.GetStringSlice(c.NS, "grants")
if err != nil {
return err
}

parsedGrants, err := parseGrantsFromArg(grants)
if err != nil {
return err
}

r := &godo.SpacesKeyCreateRequest{
Name: c.Args[0],
Grants: parsedGrants,
}

key, err := c.SpacesKeys().Create(r)
if err != nil {
return err
}

return displaySpacesKeys(c, *key)
}

func spacesKeysList(c *CmdConfig) error {
keys, err := c.SpacesKeys().List()
if err != nil {
return err
}

return displaySpacesKeys(c, keys...)
}

func spacesKeysDelete(c *CmdConfig) error {
if len(c.Args) == 0 {
return doctl.NewMissingArgsErr(c.NS)
}

err := c.SpacesKeys().Delete(c.Args[0])
if err != nil {
return err
}

return nil
}

func spacesKeysUpdate(c *CmdConfig) error {
if len(c.Args) == 0 {
return doctl.NewMissingArgsErr(c.NS)
}

grants, err := c.Doit.GetStringSlice(c.NS, "grants")
if err != nil {
return err
}

parsedGrants, err := parseGrantsFromArg(grants)
if err != nil {
return err
}

name, err := c.Doit.GetString(c.NS, "name")
if err != nil {
return err
}

r := &godo.SpacesKeyUpdateRequest{
Name: name,
Grants: parsedGrants,
}

key, err := c.SpacesKeys().Update(c.Args[0], r)
if err != nil {
return err
}

return displaySpacesKeys(c, *key)
}

func displaySpacesKeys(c *CmdConfig, keys ...do.SpacesKey) error {
item := &displayers.SpacesKey{SpacesKeys: keys}
return c.Display(item)
}

func parseGrantsFromArg(grants []string) ([]*godo.Grant, error) {
parsedGrants := []*godo.Grant{}
for _, grant := range grants {
parsedGrant, err := parseGrant(grant)
if err != nil {
return nil, err
}
parsedGrants = append(parsedGrants, parsedGrant)
}
return parsedGrants, nil
}

func parseGrant(grant string) (*godo.Grant, error) {
const (
argSeparator = ";"
kvSeparator = "="
)
trimmedGrant := strings.TrimSuffix(grant, argSeparator)
parsedGrant := &godo.Grant{
Bucket: "",
Permission: "",
}
for _, arg := range strings.Split(trimmedGrant, argSeparator) {
kv := strings.Split(arg, kvSeparator)
if len(kv) != 2 {
return nil, fmt.Errorf("A Grant must be in the format 'key=value'. Provided: %v", kv)
}

key := kv[0]
value := kv[1]

switch key {
case "bucket":
parsedGrant.Bucket = value
case "permission":
// Validate permission
switch value {
case "read":
parsedGrant.Permission = godo.SpacesKeyRead
case "readwrite":
parsedGrant.Permission = godo.SpacesKeyReadWrite
case "fullaccess":
parsedGrant.Permission = godo.SpacesKeyFullAccess
default:
return nil, fmt.Errorf("Unsupported permission %q", value)
}
default:
return nil, fmt.Errorf("Unsupported grant argument %q", key)
}
}
return parsedGrant, nil
}
Loading

0 comments on commit 0994d94

Please sign in to comment.