Skip to content

Commit

Permalink
Merge pull request #991 from hashicorp/TF-21287-patch-endpoint-for-ad…
Browse files Browse the repository at this point in the history
…ding-but-never-replacing-tag-bindings

Adds PATCH tag-bindings
  • Loading branch information
brandonc authored Oct 25, 2024
2 parents d1b5c33 + 84dff88 commit 73eefba
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Add support for enabling Stacks on an organization by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
* Add support for filtering by key/value tags by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
* Add support for adding/updating key/value tags by @brandonc [#991](https://github.com/hashicorp/go-tfe/pull/991)
* Add support for reading a registry module by its unique identifier by @dsa0x [#988](https://github.com/hashicorp/go-tfe/pull/988)

# v1.68.0
Expand Down
2 changes: 2 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ var (

ErrRequiredRegistryModule = errors.New("registry module is required")

ErrRequiredTagBindings = errors.New("TagBindings are required")

ErrInvalidTestRunID = errors.New("invalid value for test run id")

ErrTerraformVersionValidForPlanOnly = errors.New("setting terraform-version is only valid when plan-only is set to true")
Expand Down
15 changes: 15 additions & 0 deletions mocks/project_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions mocks/workspace_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions project.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type Projects interface {

// ListTagBindings lists all tag bindings associated with the project.
ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error)

// AddTagBindings adds or modifies the value of existing tag binding keys for a project.
AddTagBindings(ctx context.Context, projectID string, options ProjectAddTagBindingsOptions) ([]*TagBinding, error)
}

// projects implements Projects
Expand Down Expand Up @@ -113,6 +116,12 @@ type ProjectUpdateOptions struct {
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}

// ProjectAddTagBindingsOptions represents the options for adding tag bindings
// to a project.
type ProjectAddTagBindingsOptions struct {
TagBindings []*TagBinding
}

// List all projects.
func (s *projects) List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error) {
if !validStringID(&organization) {
Expand Down Expand Up @@ -209,6 +218,31 @@ func (s *projects) ListTagBindings(ctx context.Context, projectID string) ([]*Ta
return list.Items, nil
}

// AddTagBindings adds or modifies the value of existing tag binding keys for a project
func (s *projects) AddTagBindings(ctx context.Context, projectID string, options ProjectAddTagBindingsOptions) ([]*TagBinding, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}

if err := options.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("projects/%s/tag-bindings", url.PathEscape(projectID))
req, err := s.client.NewRequest("PATCH", u, options.TagBindings)
if err != nil {
return nil, err
}

var response = struct {
*Pagination
Items []*TagBinding
}{}
err = req.Do(ctx, &response)

return response.Items, err
}

// Update a project by its ID
func (s *projects) Update(ctx context.Context, projectID string, options ProjectUpdateOptions) (*Project, error) {
if !validStringID(&projectID) {
Expand Down Expand Up @@ -259,3 +293,11 @@ func (o ProjectCreateOptions) valid() error {
func (o ProjectUpdateOptions) valid() error {
return nil
}

func (o ProjectAddTagBindingsOptions) valid() error {
if len(o.TagBindings) == 0 {
return ErrRequiredTagBindings
}

return nil
}
72 changes: 68 additions & 4 deletions projects_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,22 @@ func TestProjectsList(t *testing.T) {
t.Run("when using a tags filter", func(t *testing.T) {
skipUnlessBeta(t)

p1, wTestCleanup1 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
p1, pTestCleanup1 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
p2, wTestCleanup2 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
p2, pTestCleanup2 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(wTestCleanup1)
t.Cleanup(wTestCleanup2)
t.Cleanup(pTestCleanup1)
t.Cleanup(pTestCleanup2)

// List all the workspaces under the given tag
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
Expand Down Expand Up @@ -247,6 +247,70 @@ func TestProjectsUpdate(t *testing.T) {
})
}

func TestProjectsAddTagBindings(t *testing.T) {
skipUnlessBeta(t)

client := testClient(t)
ctx := context.Background()

pTest, wCleanup := createProject(t, client, nil)
t.Cleanup(wCleanup)

t.Run("when adding tag bindings to a project", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}

bindings, err := client.Projects.AddTagBindings(ctx, pTest.ID, ProjectAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)

require.Len(t, bindings, 2)
assert.Equal(t, tagBindings[0].Key, bindings[0].Key)
assert.Equal(t, tagBindings[0].Value, bindings[0].Value)
assert.Equal(t, tagBindings[1].Key, bindings[1].Key)
assert.Equal(t, tagBindings[1].Value, bindings[1].Value)
})

t.Run("when adding 26 tags", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "alpha"},
{Key: "bravo"},
{Key: "charlie"},
{Key: "delta"},
{Key: "echo"},
{Key: "foxtrot"},
{Key: "golf"},
{Key: "hotel"},
{Key: "india"},
{Key: "juliet"},
{Key: "kilo"},
{Key: "lima"},
{Key: "mike"},
{Key: "november"},
{Key: "oscar"},
{Key: "papa"},
{Key: "quebec"},
{Key: "romeo"},
{Key: "sierra"},
{Key: "tango"},
{Key: "uniform"},
{Key: "victor"},
{Key: "whiskey"},
{Key: "xray"},
{Key: "yankee"},
{Key: "zulu"},
}

_, err := client.Workspaces.AddTagBindings(ctx, pTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.Error(t, err, "cannot exceed 10 bindings per resource")
})
}

func TestProjectsDelete(t *testing.T) {
client := testClient(t)
ctx := context.Background()
Expand Down
42 changes: 42 additions & 0 deletions workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ type Workspaces interface {

// ListTagBindings lists all tag bindings associated with the workspace.
ListTagBindings(ctx context.Context, workspaceID string) ([]*TagBinding, error)

// AddTagBindings adds or modifies the value of existing tag binding keys for a workspace.
AddTagBindings(ctx context.Context, workspaceID string, options WorkspaceAddTagBindingsOptions) ([]*TagBinding, error)
}

// workspaces implements Workspaces.
Expand All @@ -147,6 +150,12 @@ type WorkspaceList struct {
Items []*Workspace
}

// WorkspaceAddTagBindingsOptions represents the options for adding tag bindings
// to a workspace.
type WorkspaceAddTagBindingsOptions struct {
TagBindings []*TagBinding
}

// LockedByChoice is a choice type struct that represents the possible values
// within a polymorphic relation. If a value is available, exactly one field
// will be non-nil.
Expand Down Expand Up @@ -760,6 +769,31 @@ func (s *workspaces) ListTagBindings(ctx context.Context, workspaceID string) ([
return list.Items, nil
}

// AddTagBindings adds or modifies the value of existing tag binding keys for a workspace.
func (s *workspaces) AddTagBindings(ctx context.Context, workspaceID string, options WorkspaceAddTagBindingsOptions) ([]*TagBinding, error) {
if !validStringID(&workspaceID) {
return nil, ErrInvalidWorkspaceID
}

if err := options.valid(); err != nil {
return nil, err
}

u := fmt.Sprintf("workspaces/%s/tag-bindings", url.PathEscape(workspaceID))
req, err := s.client.NewRequest("PATCH", u, options.TagBindings)
if err != nil {
return nil, err
}

var response = struct {
*Pagination
Items []*TagBinding
}{}
err = req.Do(ctx, &response)

return response.Items, err
}

// Create is used to create a new workspace.
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
if !validStringID(&organization) {
Expand Down Expand Up @@ -1465,6 +1499,14 @@ func (s *workspaces) DeleteDataRetentionPolicy(ctx context.Context, workspaceID
return req.Do(ctx, nil)
}

func (o WorkspaceAddTagBindingsOptions) valid() error {
if len(o.TagBindings) == 0 {
return ErrRequiredTagBindings
}

return nil
}

func (o WorkspaceCreateOptions) valid() error {
if !validString(o.Name) {
return ErrRequiredName
Expand Down
64 changes: 64 additions & 0 deletions workspace_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,70 @@ func TestWorkspacesReadByID(t *testing.T) {
})
}

func TestWorkspacesAddTagBindings(t *testing.T) {
skipUnlessBeta(t)

client := testClient(t)
ctx := context.Background()

wTest, wCleanup := createWorkspace(t, client, nil)
t.Cleanup(wCleanup)

t.Run("when adding tag bindings to a workspace", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: "qux"},
}

bindings, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.NoError(t, err)

require.Len(t, bindings, 2)
assert.Equal(t, tagBindings[0].Key, bindings[0].Key)
assert.Equal(t, tagBindings[0].Value, bindings[0].Value)
assert.Equal(t, tagBindings[1].Key, bindings[1].Key)
assert.Equal(t, tagBindings[1].Value, bindings[1].Value)
})

t.Run("when adding 26 tags", func(t *testing.T) {
tagBindings := []*TagBinding{
{Key: "alpha"},
{Key: "bravo"},
{Key: "charlie"},
{Key: "delta"},
{Key: "echo"},
{Key: "foxtrot"},
{Key: "golf"},
{Key: "hotel"},
{Key: "india"},
{Key: "juliet"},
{Key: "kilo"},
{Key: "lima"},
{Key: "mike"},
{Key: "november"},
{Key: "oscar"},
{Key: "papa"},
{Key: "quebec"},
{Key: "romeo"},
{Key: "sierra"},
{Key: "tango"},
{Key: "uniform"},
{Key: "victor"},
{Key: "whiskey"},
{Key: "xray"},
{Key: "yankee"},
{Key: "zulu"},
}

_, err := client.Workspaces.AddTagBindings(ctx, wTest.ID, WorkspaceAddTagBindingsOptions{
TagBindings: tagBindings,
})
require.Error(t, err, "cannot exceed 10 bindings per resource")
})
}

func TestWorkspacesUpdate(t *testing.T) {
client := testClient(t)
ctx := context.Background()
Expand Down

0 comments on commit 73eefba

Please sign in to comment.