Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support Spaces Keys API #768

Merged
merged 4 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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"
)
lee-aaron marked this conversation as resolved.
Show resolved Hide resolved

// 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"`
}
lee-aaron marked this conversation as resolved.
Show resolved Hide resolved

// 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) {
lee-aaron marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading