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