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 all 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
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)
}
Loading