From cb33e3e6b53efd25c4d8b5a1a6e22e3bcdf6ebc1 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Sat, 7 Jan 2023 13:09:44 +0200 Subject: [PATCH 1/5] SCALRCORE-21783 Add ServiceAccountTokens service --- access_token.go | 19 +++++- access_token_test.go | 2 +- agent_pool_token.go | 42 +++---------- agent_pool_token_test.go | 16 ++--- helper_test.go | 22 ++++++- scalr.go | 2 + service_account_token.go | 77 +++++++++++++++++++++++ service_account_token_test.go | 115 ++++++++++++++++++++++++++++++++++ 8 files changed, 248 insertions(+), 47 deletions(-) create mode 100644 service_account_token.go create mode 100644 service_account_token_test.go diff --git a/access_token.go b/access_token.go index 5c2dfd1..19197d8 100644 --- a/access_token.go +++ b/access_token.go @@ -37,10 +37,25 @@ type AccessToken struct { Token string `jsonapi:"attr,token"` } +// AccessTokenListOptions represents the options for listing access tokens. +type AccessTokenListOptions struct { + ListOptions +} + +// AccessTokenCreateOptions represents the options for creating a new AccessToken. +type AccessTokenCreateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,access-tokens"` + + Description *string `jsonapi:"attr,description,omitempty"` +} + // AccessTokenUpdateOptions represents the options for updating an AccessToken. type AccessTokenUpdateOptions struct { - ID string `jsonapi:"primary,access-tokens"` - Description *string `jsonapi:"attr,description"` + // For internal use only! + ID string `jsonapi:"primary,access-tokens"` + + Description *string `jsonapi:"attr,description,omitempty"` } // Update is used to update an AccessToken. diff --git a/access_token_test.go b/access_token_test.go index 4de2fba..a3ab36a 100644 --- a/access_token_test.go +++ b/access_token_test.go @@ -57,7 +57,7 @@ func TestAccessTokenDelete(t *testing.T) { err := client.AccessTokens.Delete(ctx, apt.ID) require.NoError(t, err) - l, err := client.AgentPoolTokens.List(ctx, ap.ID, AgentPoolTokenListOptions{}) + l, err := client.AgentPoolTokens.List(ctx, ap.ID, AccessTokenListOptions{}) assert.Len(t, l.Items, 0) }) diff --git a/agent_pool_token.go b/agent_pool_token.go index c1425a2..81b9877 100644 --- a/agent_pool_token.go +++ b/agent_pool_token.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/url" - "time" ) // Compile-time proof of interface implementation. @@ -13,8 +12,8 @@ var _ AgentPoolTokens = (*agentPoolTokens)(nil) // AgentPoolTokens describes all the access token related methods that the // Scalr IACP API supports. type AgentPoolTokens interface { - List(ctx context.Context, agentPoolID string, options AgentPoolTokenListOptions) (*AgentPoolTokenList, error) - Create(ctx context.Context, agentPoolID string, options AgentPoolTokenCreateOptions) (*AgentPoolToken, error) + List(ctx context.Context, agentPoolID string, options AccessTokenListOptions) (*AccessTokenList, error) + Create(ctx context.Context, agentPoolID string, options AccessTokenCreateOptions) (*AccessToken, error) } // agentPoolTokens implements AgentPoolTokens. @@ -22,39 +21,14 @@ type agentPoolTokens struct { client *Client } -// AgentPoolTokenList represents a list of agent pools. -type AgentPoolTokenList struct { - *Pagination - Items []*AgentPoolToken -} - -// AgentPoolToken represents a Scalr agent pool. -type AgentPoolToken struct { - ID string `jsonapi:"primary,access-tokens"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - Description string `jsonapi:"attr,description"` - Token string `jsonapi:"attr,token"` -} - -// AgentPoolTokenCreateOptions represents the options for creating a new AgentPoolToken. -type AgentPoolTokenListOptions struct { - ListOptions -} - -// AgentPoolTokenCreateOptions represents the options for creating a new AgentPoolToken. -type AgentPoolTokenCreateOptions struct { - ID string `jsonapi:"primary,access-tokens"` - Description *string `jsonapi:"attr,description,omitempty"` -} - -// List all the agent pools. -func (s *agentPoolTokens) List(ctx context.Context, agentPoolID string, options AgentPoolTokenListOptions) (*AgentPoolTokenList, error) { +// List all the agent pool's tokens. +func (s *agentPoolTokens) List(ctx context.Context, agentPoolID string, options AccessTokenListOptions) (*AccessTokenList, error) { req, err := s.client.newRequest("GET", fmt.Sprintf("agent-pools/%s/access-tokens", url.QueryEscape(agentPoolID)), &options) if err != nil { return nil, err } - tl := &AgentPoolTokenList{} + tl := &AccessTokenList{} err = s.client.do(ctx, req, tl) if err != nil { return nil, err @@ -63,8 +37,8 @@ func (s *agentPoolTokens) List(ctx context.Context, agentPoolID string, options return tl, nil } -// Create is used to create a new AgentPoolToken. -func (s *agentPoolTokens) Create(ctx context.Context, agentPoolID string, options AgentPoolTokenCreateOptions) (*AgentPoolToken, error) { +// Create is used to create a new AccessToken for AgentPool. +func (s *agentPoolTokens) Create(ctx context.Context, agentPoolID string, options AccessTokenCreateOptions) (*AccessToken, error) { // Make sure we don't send a user provided ID. options.ID = "" @@ -78,7 +52,7 @@ func (s *agentPoolTokens) Create(ctx context.Context, agentPoolID string, option return nil, err } - agentPoolToken := &AgentPoolToken{} + agentPoolToken := &AccessToken{} err = s.client.do(ctx, req, agentPoolToken) if err != nil { return nil, err diff --git a/agent_pool_token_test.go b/agent_pool_token_test.go index da10366..112a674 100644 --- a/agent_pool_token_test.go +++ b/agent_pool_token_test.go @@ -20,13 +20,13 @@ func TestAgentPoolTokenList(t *testing.T) { defer aptCleanup() t.Run("with valid agent pool", func(t *testing.T) { - tList, err := client.AgentPoolTokens.List(ctx, ap.ID, AgentPoolTokenListOptions{}) + tList, err := client.AgentPoolTokens.List(ctx, ap.ID, AccessTokenListOptions{}) require.NoError(t, err) assert.Len(t, tList.Items, 1) assert.Equal(t, tList.Items[0].ID, apt.ID) }) t.Run("with nonexistent agent pool", func(t *testing.T) { - _, err := client.AgentPoolTokens.List(ctx, "ap-123", AgentPoolTokenListOptions{}) + _, err := client.AgentPoolTokens.List(ctx, "ap-123", AccessTokenListOptions{}) assert.Equal( t, ResourceNotFoundError{ @@ -45,7 +45,7 @@ func TestAgentPoolTokenCreate(t *testing.T) { defer apCleanup() t.Run("when description is provided", func(t *testing.T) { - options := AgentPoolTokenCreateOptions{ + options := AccessTokenCreateOptions{ Description: String("provider tests token"), } @@ -53,7 +53,7 @@ func TestAgentPoolTokenCreate(t *testing.T) { require.NoError(t, err) // Get a refreshed view from the API. - aptList, err := client.AgentPoolTokens.List(ctx, ap.ID, AgentPoolTokenListOptions{}) + aptList, err := client.AgentPoolTokens.List(ctx, ap.ID, AccessTokenListOptions{}) require.NoError(t, err) refreshed := aptList.Items[0] @@ -66,12 +66,12 @@ func TestAgentPoolTokenCreate(t *testing.T) { }) t.Run("when description is not provided", func(t *testing.T) { - options := AgentPoolTokenCreateOptions{} + options := AccessTokenCreateOptions{} apToken, err := client.AgentPoolTokens.Create(ctx, ap.ID, options) require.NoError(t, err) // Get a refreshed view from the API. - aptList, err := client.AgentPoolTokens.List(ctx, ap.ID, AgentPoolTokenListOptions{}) + aptList, err := client.AgentPoolTokens.List(ctx, ap.ID, AccessTokenListOptions{}) require.NoError(t, err) refreshed := aptList.Items[0] @@ -85,7 +85,7 @@ func TestAgentPoolTokenCreate(t *testing.T) { t.Run("with nonexistent pool id", func(t *testing.T) { var apID = "ap-234" - _, err := client.AgentPoolTokens.Create(ctx, apID, AgentPoolTokenCreateOptions{}) + _, err := client.AgentPoolTokens.Create(ctx, apID, AccessTokenCreateOptions{}) assert.Equal( t, ResourceNotFoundError{ @@ -97,7 +97,7 @@ func TestAgentPoolTokenCreate(t *testing.T) { t.Run("with invalid pool id", func(t *testing.T) { apID := badIdentifier - ap, err := client.AgentPoolTokens.Create(ctx, apID, AgentPoolTokenCreateOptions{}) + ap, err := client.AgentPoolTokens.Create(ctx, apID, AccessTokenCreateOptions{}) assert.Nil(t, ap) assert.EqualError(t, err, fmt.Sprintf("invalid value for agent pool ID: '%s'", apID)) diff --git a/helper_test.go b/helper_test.go index 64ef025..420cb16 100644 --- a/helper_test.go +++ b/helper_test.go @@ -63,9 +63,9 @@ func createAgentPool(t *testing.T, client *Client) (*AgentPool, func()) { } } -func createAgentPoolToken(t *testing.T, client *Client, poolID string) (*AgentPoolToken, func()) { +func createAgentPoolToken(t *testing.T, client *Client, poolID string) (*AccessToken, func()) { ctx := context.Background() - apt, err := client.AgentPoolTokens.Create(ctx, poolID, AgentPoolTokenCreateOptions{Description: String("provider test token")}) + apt, err := client.AgentPoolTokens.Create(ctx, poolID, AccessTokenCreateOptions{Description: String("provider test token")}) if err != nil { t.Fatal(err) } @@ -442,6 +442,24 @@ func createServiceAccount( } } +func createServiceAccountToken(t *testing.T, client *Client, serviceAccountID string) (*AccessToken, func()) { + ctx := context.Background() + sat, err := client.ServiceAccountTokens.Create( + ctx, serviceAccountID, AccessTokenCreateOptions{Description: String("tst-description-" + randomString(t))}, + ) + if err != nil { + t.Fatal(err) + } + + return sat, func() { + if err := client.AccessTokens.Delete(ctx, sat.ID); err != nil { + t.Errorf("Error destroying service account token! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Service account token: %s\nError: %s", sat.ID, err) + } + } +} + func assignTagsToWorkspace(t *testing.T, client *Client, workspace *Workspace, tags []*Tag) { ctx := context.Background() tagRels := make([]*TagRelation, len(tags)) diff --git a/scalr.go b/scalr.go index a994b4d..901a61e 100644 --- a/scalr.go +++ b/scalr.go @@ -136,6 +136,7 @@ type Client struct { Roles Roles RunTriggers RunTriggers Runs Runs + ServiceAccountTokens ServiceAccountTokens ServiceAccounts ServiceAccounts Tags Tags Teams Teams @@ -232,6 +233,7 @@ func NewClient(cfg *Config) (*Client, error) { client.Roles = &roles{client: client} client.RunTriggers = &runTriggers{client: client} client.Runs = &runs{client: client} + client.ServiceAccountTokens = &serviceAccountTokens{client: client} client.ServiceAccounts = &serviceAccounts{client: client} client.Tags = &tags{client: client} client.Teams = &teams{client: client} diff --git a/service_account_token.go b/service_account_token.go new file mode 100644 index 0000000..a41e4bd --- /dev/null +++ b/service_account_token.go @@ -0,0 +1,77 @@ +package scalr + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ ServiceAccountTokens = (*serviceAccountTokens)(nil) + +// ServiceAccountTokens describes all the access token related methods that the +// Scalr IACP API supports. +type ServiceAccountTokens interface { + // List service account's access tokens + List(ctx context.Context, serviceAccountID string, options AccessTokenListOptions) (*AccessTokenList, error) + // Create new access token for service account + Create(ctx context.Context, serviceAccountID string, options AccessTokenCreateOptions) (*AccessToken, error) +} + +// serviceAccountTokens implements ServiceAccountTokens. +type serviceAccountTokens struct { + client *Client +} + +// List the access tokens of ServiceAccount. +func (s *serviceAccountTokens) List( + ctx context.Context, serviceAccountID string, options AccessTokenListOptions, +) (*AccessTokenList, error) { + req, err := s.client.newRequest( + "GET", + fmt.Sprintf("service-accounts/%s/access-tokens", url.QueryEscape(serviceAccountID)), + &options, + ) + if err != nil { + return nil, err + } + + atl := &AccessTokenList{} + err = s.client.do(ctx, req, atl) + if err != nil { + return nil, err + } + + return atl, nil +} + +// Create is used to create a new AccessToken for ServiceAccount. +func (s *serviceAccountTokens) Create( + ctx context.Context, serviceAccountID string, options AccessTokenCreateOptions, +) (*AccessToken, error) { + + // Make sure we don't send a user provided ID. + options.ID = "" + + if !validStringID(&serviceAccountID) { + return nil, errors.New("invalid value for service account ID") + } + + req, err := s.client.newRequest( + "POST", + fmt.Sprintf("service-accounts/%s/access-tokens", url.QueryEscape(serviceAccountID)), + &options, + ) + if err != nil { + return nil, err + } + + at := &AccessToken{} + err = s.client.do(ctx, req, at) + if err != nil { + return nil, err + } + + return at, nil +} diff --git a/service_account_token_test.go b/service_account_token_test.go new file mode 100644 index 0000000..48f2765 --- /dev/null +++ b/service_account_token_test.go @@ -0,0 +1,115 @@ +package scalr + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestServiceAccountTokenList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + sa, saCleanup := createServiceAccount( + t, client, &Account{ID: defaultAccountID}, ServiceAccountStatusPtr(ServiceAccountStatusActive), + ) + defer saCleanup() + + at1, at1Cleanup := createServiceAccountToken(t, client, sa.ID) + defer at1Cleanup() + + at2, at2Cleanup := createServiceAccountToken(t, client, sa.ID) + defer at2Cleanup() + + t.Run("with valid service account", func(t *testing.T) { + atl, err := client.ServiceAccountTokens.List(ctx, sa.ID, AccessTokenListOptions{}) + require.NoError(t, err) + assert.Equal(t, 2, atl.TotalCount) + + atIDs := make([]string, len(atl.Items)) + for i, at := range atl.Items { + atIDs[i] = at.ID + } + assert.Contains(t, atIDs, at1.ID) + assert.Contains(t, atIDs, at2.ID) + }) + t.Run("with nonexistent service account", func(t *testing.T) { + var saId = "notexisting" + _, err := client.ServiceAccountTokens.List(ctx, saId, AccessTokenListOptions{}) + assert.Equal( + t, + ResourceNotFoundError{ + Message: fmt.Sprintf("ServiceAccount with ID '%s' not found or user unauthorized", saId), + }.Error(), + err.Error(), + ) + }) +} + +func TestServiceAccountTokenCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + sa, saCleanup := createServiceAccount( + t, client, &Account{ID: defaultAccountID}, ServiceAccountStatusPtr(ServiceAccountStatusActive), + ) + defer saCleanup() + + t.Run("when description is provided", func(t *testing.T) { + options := AccessTokenCreateOptions{ + Description: String("tst-description-" + randomString(t)), + } + + at, err := client.ServiceAccountTokens.Create(ctx, sa.ID, options) + require.NoError(t, err) + + defer func() { _ = client.AccessTokens.Delete(ctx, at.ID) }() + + // Get a refreshed view from the API. + atl, err := client.ServiceAccountTokens.List(ctx, sa.ID, AccessTokenListOptions{}) + require.NoError(t, err) + + refreshed := atl.Items[0] + + assert.NotEmpty(t, refreshed.ID) + assert.Equal(t, *options.Description, refreshed.Description) + }) + + t.Run("when description is not provided", func(t *testing.T) { + options := AccessTokenCreateOptions{} + + at, err := client.ServiceAccountTokens.Create(ctx, sa.ID, options) + require.NoError(t, err) + + defer func() { _ = client.AccessTokens.Delete(ctx, at.ID) }() + + // Get a refreshed view from the API. + atl, err := client.ServiceAccountTokens.List(ctx, sa.ID, AccessTokenListOptions{}) + require.NoError(t, err) + + refreshed := atl.Items[0] + + assert.NotEmpty(t, refreshed.ID) + assert.Equal(t, refreshed.Description, "") + }) + + t.Run("with nonexistent service account id", func(t *testing.T) { + var saID = "notexisting" + _, err := client.ServiceAccountTokens.Create(ctx, saID, AccessTokenCreateOptions{}) + assert.Equal( + t, + ResourceNotFoundError{ + Message: fmt.Sprintf("ServiceAccount with ID '%s' not found or user unauthorized", saID), + }.Error(), + err.Error(), + ) + }) + + t.Run("with invalid service account id", func(t *testing.T) { + _, err := client.ServiceAccountTokens.Create(ctx, badIdentifier, AccessTokenCreateOptions{}) + assert.EqualError(t, err, "invalid value for service account ID") + }) +} From 1d3b758bfc1bc907c16499bdbdc24b45f35189d6 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Sat, 7 Jan 2023 13:44:56 +0200 Subject: [PATCH 2/5] SCALRCORE-21783 Add AccessTokens.Read method --- access_token.go | 22 ++++++++++++++++++++++ access_token_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/access_token.go b/access_token.go index 19197d8..27f1c96 100644 --- a/access_token.go +++ b/access_token.go @@ -14,6 +14,7 @@ var _ AccessTokens = (*accessTokens)(nil) // AccessTokens describes all the access token related methods that the // Scalr IACP API supports. type AccessTokens interface { + Read(ctx context.Context, accessTokenID string) (*AccessToken, error) Update(ctx context.Context, accessTokenID string, options AccessTokenUpdateOptions) (*AccessToken, error) Delete(ctx context.Context, accessTokenID string) error } @@ -58,6 +59,27 @@ type AccessTokenUpdateOptions struct { Description *string `jsonapi:"attr,description,omitempty"` } +// Read access token by its ID +func (s *accessTokens) Read(ctx context.Context, accessTokenID string) (*AccessToken, error) { + if !validStringID(&accessTokenID) { + return nil, errors.New("invalid value for access token ID") + } + + u := fmt.Sprintf("access-tokens/%s", url.QueryEscape(accessTokenID)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + at := &AccessToken{} + err = s.client.do(ctx, req, at) + if err != nil { + return nil, err + } + + return at, nil +} + // Update is used to update an AccessToken. func (s *accessTokens) Update(ctx context.Context, accessTokenID string, options AccessTokenUpdateOptions) (*AccessToken, error) { diff --git a/access_token_test.go b/access_token_test.go index a3ab36a..824f7de 100644 --- a/access_token_test.go +++ b/access_token_test.go @@ -9,6 +9,33 @@ import ( "github.com/stretchr/testify/require" ) +func TestAccessTokenRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + ap, apCleanup := createAgentPool(t, client) + defer apCleanup() + + atTest, atTestCleanup := createAgentPoolToken(t, client, ap.ID) + defer atTestCleanup() + + t.Run("when the token exists", func(t *testing.T) { + at, err := client.AccessTokens.Read(ctx, atTest.ID) + require.NoError(t, err) + assert.Equal(t, atTest.ID, at.ID) + }) + + t.Run("when the token does not exist", func(t *testing.T) { + _, err := client.AccessTokens.Read(ctx, "at-nonexisting") + assert.Error(t, err) + }) + + t.Run("with invalid token ID", func(t *testing.T) { + _, err := client.AccessTokens.Read(ctx, badIdentifier) + assert.EqualError(t, err, "invalid value for access token ID") + }) +} + func TestAccessTokenUpdate(t *testing.T) { client := testClient(t) ctx := context.Background() From a9e0dbf61816201fb4008c8e3000ccf21f9828e8 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Sat, 7 Jan 2023 13:47:06 +0200 Subject: [PATCH 3/5] Trigger tests [API_BRANCH] From a086d61e2c02060ab1e98efbd004397581c7d14f Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Wed, 11 Jan 2023 14:51:36 +0200 Subject: [PATCH 4/5] SCALRCORE-21783 Add code owners [API_BRANCH] --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..222ac9a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default global owners +* v.mihun@scalr.com p.protsakh@scalr.com From acdac16a6fc804efcbc718876e83e9d20321a312 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Fri, 13 Jan 2023 14:14:56 +0200 Subject: [PATCH 5/5] SCALRCORE-21783 Remove token description validation [API_BRANCH] --- access_token.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/access_token.go b/access_token.go index 27f1c96..3569a91 100644 --- a/access_token.go +++ b/access_token.go @@ -90,10 +90,6 @@ func (s *accessTokens) Update(ctx context.Context, accessTokenID string, options return nil, fmt.Errorf("invalid value for access token ID: '%s'", accessTokenID) } - if !validString(options.Description) { - return nil, errors.New("value for description must be a valid string") - } - req, err := s.client.newRequest("PATCH", fmt.Sprintf("access-tokens/%s", url.QueryEscape(accessTokenID)), &options) if err != nil { return nil, err