Skip to content

Commit

Permalink
Filtering by and updating tag-bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonc committed Oct 16, 2024
1 parent d344210 commit f28ec28
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Unreleased

## Enhancements

* Add support for enabling Stacks on an organization by @brandonc
* Add support for filtering by key/value tags by @brandonc

# v1.68.0

## Enhancements
Expand Down
10 changes: 7 additions & 3 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2519,16 +2519,20 @@ func upgradeOrganizationSubscription(t *testing.T, _ *Client, organization *Orga
}

func createProject(t *testing.T, client *Client, org *Organization) (*Project, func()) {
return createProjectWithOptions(t, client, org, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
})
}

func createProjectWithOptions(t *testing.T, client *Client, org *Organization, options ProjectCreateOptions) (*Project, func()) {
var orgCleanup func()

if org == nil {
org, orgCleanup = createOrganization(t, client)
}

ctx := context.Background()
p, err := client.Projects.Create(ctx, org.Name, ProjectCreateOptions{
Name: randomStringWithoutSpecialChar(t),
})
p, err := client.Projects.Create(ctx, org.Name, options)
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 4 additions & 0 deletions organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ type OrganizationUpdateOptions struct {

// Optional: DefaultAgentPoolId default agent pool for workspaces, requires DefaultExecutionMode to be set to `agent`
DefaultAgentPool *AgentPool `jsonapi:"relation,default-agent-pool,omitempty"`

// Optional: StacksEnabled toggles whether stacks are enabled for the organization. This setting
// is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
StacksEnabled *bool `jsonapi:"attr,stacks-enabled,omitempty"`
}

// ReadRunQueueOptions represents the options for showing the queue.
Expand Down
14 changes: 13 additions & 1 deletion project.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ type ProjectListOptions struct {

// Optional: A query string to search projects by names.
Query string `url:"q,omitempty"`

// Optional: A filter string to list projects filtered by key/value tags.
// These are not annotated and therefore not encoded by go-querystring
TagBindings []*TagBinding
}

// ProjectCreateOptions represents the options for creating a project
Expand All @@ -82,6 +86,9 @@ type ProjectCreateOptions struct {

// Optional: A description for the project.
Description *string `jsonapi:"attr,description,omitempty"`

// Associated TagBindings of the project.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}

// ProjectUpdateOptions represents the options for updating a project
Expand All @@ -105,8 +112,13 @@ func (s *projects) List(ctx context.Context, organization string, options *Proje
return nil, ErrInvalidOrg
}

var tagFilters map[string][]string
if options != nil {
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
}

u := fmt.Sprintf("organizations/%s/projects", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
if err != nil {
return nil, err
}
Expand Down
47 changes: 47 additions & 0 deletions projects_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,53 @@ func TestProjectsList(t *testing.T) {
assert.Nil(t, pl)
assert.EqualError(t, err, ErrInvalidOrg.Error())
})

t.Run("when using a tags filter", func(t *testing.T) {
p1, wTestCleanup1 := 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{
Name: randomStringWithoutSpecialChar(t),
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(wTestCleanup1)
t.Cleanup(wTestCleanup2)

// List all the workspaces under the given tag
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key1"},
},
})
assert.NoError(t, err)
assert.Len(t, pl.Items, 1)
assert.Contains(t, pl.Items, p1)

pl2, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key2"},
},
})
assert.NoError(t, err)
assert.Len(t, pl2.Items, 2)
assert.Contains(t, pl2.Items, p1, p2)

pl3, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
},
})
assert.NoError(t, err)
assert.Len(t, pl3.Items, 1)
assert.Contains(t, pl3.Items, p2)
})
}

func TestProjectsRead(t *testing.T) {
Expand Down
22 changes: 22 additions & 0 deletions tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package tfe

import "fmt"

type TagList struct {
*Pagination
Items []*Tag
Expand All @@ -13,3 +15,23 @@ type Tag struct {
ID string `jsonapi:"primary,tags"`
Name string `jsonapi:"attr,name,omitempty"`
}

type TagBinding struct {
ID string `jsonapi:"primary,tag-bindings"`
Key string `jsonapi:"attr,key"`
Value string `jsonapi:"attr,value,omitempty"`
}

func encodeTagFiltersAsParams(filters []*TagBinding) map[string][]string {
if len(filters) == 0 {
return nil
}

var tagFilter map[string][]string = make(map[string][]string, len(filters))

Check failure on line 30 in tag.go

View workflow job for this annotation

GitHub Actions / Lint

ST1023: should omit type map[string][]string from declaration; it will be inferred from the right-hand side (stylecheck)
for index, tag := range filters {
tagFilter[fmt.Sprintf("filter[tagged][%d][key]", index)] = []string{tag.Key}
tagFilter[fmt.Sprintf("filter[tagged][%d][value]", index)] = []string{tag.Value}
}

return tagFilter
}
24 changes: 21 additions & 3 deletions workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ type Workspace struct {
CurrentConfigurationVersion *ConfigurationVersion `jsonapi:"relation,current-configuration-version,omitempty"`
LockedBy *LockedByChoice `jsonapi:"polyrelation,locked-by"`
Variables []*Variable `jsonapi:"relation,vars"`
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"`

// Deprecated: Use DataRetentionPolicyChoice instead.
DataRetentionPolicy *DataRetentionPolicy
Expand Down Expand Up @@ -329,6 +330,10 @@ type WorkspaceListOptions struct {
// Optional: A filter string to list all the workspaces filtered by current run status.
CurrentRunStatus string `url:"filter[current-run][status],omitempty"`

// Optional: A filter string to list workspaces filtered by key/value tags.
// These are not annotated and therefore not encoded by go-querystring
TagBindings []*TagBinding

// Optional: A list of relations to include. See available resources https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#available-related-resources
Include []WSIncludeOpt `url:"include,omitempty"`

Expand Down Expand Up @@ -471,6 +476,9 @@ type WorkspaceCreateOptions struct {
// Associated Project with the workspace. If not provided, default project
// of the organization will be assigned to the workspace.
Project *Project `jsonapi:"relation,project,omitempty"`

// Associated TagBindings of the workspace.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}

// TODO: move this struct out. VCSRepoOptions is used by workspaces, policy sets, and registry modules
Expand Down Expand Up @@ -610,6 +618,10 @@ type WorkspaceUpdateOptions struct {
// Associated Project with the workspace. If not provided, default project
// of the organization will be assigned to the workspace
Project *Project `jsonapi:"relation,project,omitempty"`

// Associated TagBindings of the project. Note that this will replace
// all existing tag bindings.
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
}

// WorkspaceLockOptions represents the options for locking a workspace.
Expand Down Expand Up @@ -700,8 +712,14 @@ func (s *workspaces) List(ctx context.Context, organization string, options *Wor
return nil, err
}

var tagFilters map[string][]string
if options != nil {
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
}

// Encode parameters that cannot be encoded by go-querystring
u := fmt.Sprintf("organizations/%s/workspaces", url.PathEscape(organization))
req, err := s.client.NewRequest("GET", u, options)
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1436,7 +1454,7 @@ func (o WorkspaceCreateOptions) valid() error {
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
return ErrRequiredAgentPoolID
}
if o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 &&
if len(o.TriggerPrefixes) > 0 &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTriggerPatternsAndPrefixes
}
Expand Down Expand Up @@ -1466,7 +1484,7 @@ func (o WorkspaceUpdateOptions) valid() error {
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
return ErrRequiredAgentPoolID
}
if o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 &&
if len(o.TriggerPrefixes) > 0 &&
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
return ErrUnsupportedBothTriggerPatternsAndPrefixes
}
Expand Down
53 changes: 53 additions & 0 deletions workspace_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,53 @@ func TestWorkspacesList(t *testing.T) {
assert.Equal(t, 0, wl.TotalCount)
})

t.Run("when using a tags filter", func(t *testing.T) {
w1, wTestCleanup1 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TagBindings: []*TagBinding{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2a"},
},
})
w2, wTestCleanup2 := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{
Name: String(randomString(t)),
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
{Key: "key3", Value: "value3"},
},
})
t.Cleanup(wTestCleanup1)
t.Cleanup(wTestCleanup2)

// List all the workspaces under the given tag
wl, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key1"},
},
})
assert.NoError(t, err)
assert.Len(t, wl.Items, 1)
assert.Contains(t, wl.Items, w1)

wl2, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key2"},
},
})
assert.NoError(t, err)
assert.Len(t, wl2.Items, 2)
assert.Contains(t, wl2.Items, w1, w2)

wl3, err := client.Workspaces.List(ctx, orgTest.Name, &WorkspaceListOptions{
TagBindings: []*TagBinding{
{Key: "key2", Value: "value2b"},
},
})
assert.NoError(t, err)
assert.Len(t, wl3.Items, 1)
assert.Contains(t, wl3.Items, w2)
})

t.Run("when using project id filter and project contains workspaces", func(t *testing.T) {
// create a project in the orgTest
p, pTestCleanup := createProject(t, client, orgTest)
Expand Down Expand Up @@ -1222,6 +1269,9 @@ func TestWorkspacesUpdate(t *testing.T) {
TerraformVersion: String("0.11.1"),
TriggerPrefixes: []string{"/modules", "/shared"},
WorkingDirectory: String("baz/"),
TagBindings: []*TagBinding{
{Key: "foo", Value: "bar"},
},
}

w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, options)
Expand All @@ -1248,6 +1298,9 @@ func TestWorkspacesUpdate(t *testing.T) {
assert.Equal(t, *options.TerraformVersion, item.TerraformVersion)
assert.Equal(t, options.TriggerPrefixes, item.TriggerPrefixes)
assert.Equal(t, *options.WorkingDirectory, item.WorkingDirectory)
assert.Len(t, item.TagBindings, 1)
assert.Equal(t, "foo", item.TagBindings[0].Key)
assert.Equal(t, "bar", item.TagBindings[0].Value)
}
})

Expand Down

0 comments on commit f28ec28

Please sign in to comment.