Skip to content

Commit

Permalink
support Spaces Keys API (#768)
Browse files Browse the repository at this point in the history
* support Spaces Keys API

* Update func names and permission type

---------

Co-authored-by: Andrew Starr-Bochicchio <[email protected]>
  • Loading branch information
lee-aaron and andrewsomething authored Jan 10, 2025
1 parent 1443692 commit 4a45b95
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 0 deletions.
2 changes: 2 additions & 0 deletions godo.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type Client struct {
ReservedIPV6Actions ReservedIPV6ActionsService
Sizes SizesService
Snapshots SnapshotsService
SpacesKeys SpacesKeysService
Storage StorageService
StorageActions StorageActionsService
Tags TagsService
Expand Down Expand Up @@ -303,6 +304,7 @@ func NewClient(httpClient *http.Client) *Client {
c.ReservedIPV6Actions = &ReservedIPV6ActionsServiceOp{client: c}
c.Sizes = &SizesServiceOp{client: c}
c.Snapshots = &SnapshotsServiceOp{client: c}
c.SpacesKeys = &SpacesKeysServiceOp{client: c}
c.Storage = &StorageServiceOp{client: c}
c.StorageActions = &StorageActionsServiceOp{client: c}
c.Tags = &TagsServiceOp{client: c}
Expand Down
165 changes: 165 additions & 0 deletions spaces_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package godo

import (
"context"
"fmt"
"net/http"
)

const spacesKeysBasePath = "v2/spaces/keys"

// SpacesKeysService is an interface for managing Spaces keys with the DigitalOcean API.
type SpacesKeysService interface {
List(context.Context, *ListOptions) ([]*SpacesKey, *Response, error)
Update(context.Context, string, *SpacesKeyUpdateRequest) (*SpacesKey, *Response, error)
Create(context.Context, *SpacesKeyCreateRequest) (*SpacesKey, *Response, error)
Delete(context.Context, string) (*Response, error)
}

// SpacesKeysServiceOp handles communication with the Spaces key related methods of the
// DigitalOcean API.
type SpacesKeysServiceOp struct {
client *Client
}

var _ SpacesKeysService = &SpacesKeysServiceOp{}

// SpacesKeyPermission represents a permission for a Spaces grant
type SpacesKeyPermission string

const (
// SpacesKeyRead grants read-only access to the Spaces bucket
SpacesKeyRead SpacesKeyPermission = "read"
// SpacesKeyReadWrite grants read and write access to the Spaces bucket
SpacesKeyReadWrite SpacesKeyPermission = "readwrite"
// SpacesKeyFullAccess grants full access to the Spaces bucket
SpacesKeyFullAccess SpacesKeyPermission = "fullaccess"
)

// Grant represents a Grant for a Spaces key
type Grant struct {
Bucket string `json:"bucket"`
Permission SpacesKeyPermission `json:"permission"`
}

// SpacesKey represents a DigitalOcean Spaces key
type SpacesKey struct {
Name string `json:"name"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Grants []*Grant `json:"grants"`
CreatedAt string `json:"created_at"`
}

// SpacesKeyRoot represents a response from the DigitalOcean API
type spacesKeyRoot struct {
Key *SpacesKey `json:"key"`
}

// SpacesKeyCreateRequest represents a request to create a Spaces key.
type SpacesKeyCreateRequest struct {
Name string `json:"name"`
Grants []*Grant `json:"grants"`
}

// SpacesKeyUpdateRequest represents a request to update a Spaces key.
type SpacesKeyUpdateRequest struct {
Name string `json:"name"`
Grants []*Grant `json:"grants"`
}

// spacesListKeysRoot represents a response from the DigitalOcean API
type spacesListKeysRoot struct {
Keys []*SpacesKey `json:"keys,omitempty"`
Links *Links `json:"links,omitempty"`
Meta *Meta `json:"meta"`
}

// Create creates a new Spaces key.
func (s *SpacesKeysServiceOp) Create(ctx context.Context, createRequest *SpacesKeyCreateRequest) (*SpacesKey, *Response, error) {
if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil")
}

req, err := s.client.NewRequest(ctx, http.MethodPost, spacesKeysBasePath, createRequest)
if err != nil {
return nil, nil, err
}

root := new(spacesKeyRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.Key, resp, nil
}

// Delete deletes a Spaces key.
func (s *SpacesKeysServiceOp) Delete(ctx context.Context, accessKey string) (*Response, error) {
if accessKey == "" {
return nil, NewArgError("accessKey", "cannot be empty")
}

path := fmt.Sprintf("%s/%s", spacesKeysBasePath, accessKey)
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(ctx, req, nil)
if err != nil {
return resp, err
}

return resp, nil
}

// Update updates a Spaces key.
func (s *SpacesKeysServiceOp) Update(ctx context.Context, accessKey string, updateRequest *SpacesKeyUpdateRequest) (*SpacesKey, *Response, error) {
if accessKey == "" {
return nil, nil, NewArgError("accessKey", "cannot be empty")
}
if updateRequest == nil {
return nil, nil, NewArgError("updateRequest", "cannot be nil")
}

path := fmt.Sprintf("%s/%s", spacesKeysBasePath, accessKey)
req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest)
if err != nil {
return nil, nil, err
}
root := new(spacesKeyRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

return root.Key, resp, nil
}

// List returns a list of Spaces keys.
func (s *SpacesKeysServiceOp) List(ctx context.Context, opts *ListOptions) ([]*SpacesKey, *Response, error) {
path, err := addOptions(spacesKeysBasePath, opts)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}

root := new(spacesListKeysRoot)
resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}

if root.Links != nil {
resp.Links = root.Links
}
if root.Meta != nil {
resp.Meta = root.Meta
}

return root.Keys, resp, nil
}
145 changes: 145 additions & 0 deletions spaces_keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package godo

import (
"context"
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

func TestSpacesKeyCreate(t *testing.T) {
setup()
defer teardown()

createRequest := &SpacesKeyCreateRequest{
Name: "test-key",
Grants: []*Grant{
{
Bucket: "test-bucket",
Permission: SpacesKeyRead,
},
},
}

mux.HandleFunc("/v2/spaces/keys", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, `{"key":{"name":"test-key","access_key":"test-access-key","secret_key":"test-secret-key","created_at":"2023-10-01T00:00:00Z","grants":[{"bucket":"test-bucket","permission":"read"}]}}`)
})

key, resp, err := client.SpacesKeys.Create(context.Background(), createRequest)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "test-key", key.Name)
assert.Equal(t, "test-access-key", key.AccessKey)
assert.Equal(t, "test-secret-key", key.SecretKey)
assert.Len(t, key.Grants, 1)
assert.Equal(t, "test-bucket", key.Grants[0].Bucket)
assert.Equal(t, SpacesKeyRead, key.Grants[0].Permission)
}

func TestSpacesKeyDelete(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/v2/spaces/keys/test-access-key", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method)
w.WriteHeader(http.StatusNoContent)
})

resp, err := client.SpacesKeys.Delete(context.Background(), "test-access-key")
assert.NoError(t, err)
assert.NotNil(t, resp)
}

func TestSpacesKeyUpdate(t *testing.T) {
setup()
defer teardown()

updateRequest := &SpacesKeyUpdateRequest{
Name: "updated-key",
Grants: []*Grant{
{
Bucket: "updated-bucket",
Permission: SpacesKeyReadWrite,
},
},
}

mux.HandleFunc("/v2/spaces/keys/test-access-key", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"key":{"name":"updated-key","access_key":"test-access-key","created_at":"2023-10-01T00:00:00Z","grants":[{"bucket":"updated-bucket","permission":"readwrite"}]}}`)
})

key, resp, err := client.SpacesKeys.Update(context.Background(), "test-access-key", updateRequest)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, "updated-key", key.Name)
assert.Equal(t, "test-access-key", key.AccessKey)
assert.Len(t, key.Grants, 1)
assert.Equal(t, "updated-bucket", key.Grants[0].Bucket)
assert.Equal(t, SpacesKeyReadWrite, key.Grants[0].Permission)
}

func TestSpacesKeyList(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/v2/spaces/keys", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"keys":[{"name":"test-key","access_key":"test-access-key","created_at":"2023-10-01T00:00:00Z","grants":[{"bucket":"test-bucket","permission":"read"}]}]}`)
})

keys, resp, err := client.SpacesKeys.List(context.Background(), nil)
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, keys, 1)
assert.Equal(t, "test-key", keys[0].Name)
assert.Equal(t, "test-access-key", keys[0].AccessKey)
assert.Len(t, keys[0].Grants, 1)
assert.Equal(t, "test-bucket", keys[0].Grants[0].Bucket)
assert.Equal(t, SpacesKeyRead, keys[0].Grants[0].Permission)
}

func TestSpacesKeyList_Pagination(t *testing.T) {
setup()
defer teardown()

mux.HandleFunc("/v2/spaces/keys", func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
page := r.URL.Query().Get("page")
if page == "2" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"keys":[{"name":"test-key-2","access_key":"test-access-key-2","created_at":"2023-10-02T00:00:00Z","grants":[{"bucket":"test-bucket-2","permission":"readwrite"}]}]}`)
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"keys":[{"name":"test-key-1","access_key":"test-access-key-1","created_at":"2023-10-01T00:00:00Z","grants":[{"bucket":"test-bucket-1","permission":"read"}]}]}`)
}
})

// Test first page
keys, resp, err := client.SpacesKeys.List(context.Background(), &ListOptions{Page: 1})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, keys, 1)
assert.Equal(t, "test-key-1", keys[0].Name)
assert.Equal(t, "test-access-key-1", keys[0].AccessKey)
assert.Len(t, keys[0].Grants, 1)
assert.Equal(t, "test-bucket-1", keys[0].Grants[0].Bucket)
assert.Equal(t, SpacesKeyRead, keys[0].Grants[0].Permission)

// Test second page
keys, resp, err = client.SpacesKeys.List(context.Background(), &ListOptions{Page: 2})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, keys, 1)
assert.Equal(t, "test-key-2", keys[0].Name)
assert.Equal(t, "test-access-key-2", keys[0].AccessKey)
assert.Len(t, keys[0].Grants, 1)
assert.Equal(t, "test-bucket-2", keys[0].Grants[0].Bucket)
assert.Equal(t, SpacesKeyReadWrite, keys[0].Grants[0].Permission)
}

0 comments on commit 4a45b95

Please sign in to comment.