Skip to content

Commit

Permalink
support Spaces Keys API
Browse files Browse the repository at this point in the history
  • Loading branch information
lee-aaron committed Jan 10, 2025
1 parent f5b8e97 commit 621fe13
Show file tree
Hide file tree
Showing 3 changed files with 296 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 @@ -302,6 +303,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
164 changes: 164 additions & 0 deletions spaces_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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 {
ListSpacesKeys(context.Context, *ListOptions) ([]*SpacesKey, *Response, error)
UpdateSpacesKey(context.Context, string, *SpacesKeyUpdateRequest) (*SpacesKey, *Response, error)
CreateSpacesKey(context.Context, *SpacesKeyCreateRequest) (*SpacesKey, *Response, error)
DeleteSpacesKey(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{}

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

const (
// PermissionRead grants read-only access to the Spaces bucket
PermissionRead Permission = "read"
// PermissionReadWrite grants read and write access to the Spaces bucket
PermissionReadWrite Permission = "readwrite"
// PermissionFullAccess grants full access to the Spaces bucket
PermissionFullAccess Permission = "fullaccess"
)

// Grant represents a Grant for a Spaces key
type Grant struct {
Bucket string `json:"bucket"`
Permission Permission `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"`
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"`
}

// CreateSpacesKey implements SpacesKeysService.
func (s *SpacesKeysServiceOp) CreateSpacesKey(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
}

// DeleteSpacesKey implements SpacesKeysService.
func (s *SpacesKeysServiceOp) DeleteSpacesKey(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
}

// UpdateSpacesKey
func (s *SpacesKeysServiceOp) UpdateSpacesKey(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
}

// ListSpacesKeys returns a list of Spaces keys.
func (s *SpacesKeysServiceOp) ListSpacesKeys(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
}
130 changes: 130 additions & 0 deletions spaces_keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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: PermissionRead,
},
},
}

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"}}`)
})

key, resp, err := client.SpacesKeys.CreateSpacesKey(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)
}

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.DeleteSpacesKey(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: PermissionReadWrite,
},
},
}

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"}}`)
})

key, resp, err := client.SpacesKeys.UpdateSpacesKey(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)
}

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"}]}`)
})

keys, resp, err := client.SpacesKeys.ListSpacesKeys(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)
}

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"}]}`)
} 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"}]}`)
}
})

// Test first page
keys, resp, err := client.SpacesKeys.ListSpacesKeys(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)

// Test second page
keys, resp, err = client.SpacesKeys.ListSpacesKeys(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)
}

0 comments on commit 621fe13

Please sign in to comment.