Skip to content

Commit acb6905

Browse files
committed
Filtering by and updating tag-bindings
1 parent d344210 commit acb6905

File tree

8 files changed

+244
-7
lines changed

8 files changed

+244
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
## Enhancements
4+
5+
* Add support for enabling Stacks on an organization by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
6+
* Add support for filtering by key/value tags by @brandonc [#987](https://github.com/hashicorp/go-tfe/pull/987)
7+
38
# v1.68.0
49

510
## Enhancements

helper_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,16 +2519,20 @@ func upgradeOrganizationSubscription(t *testing.T, _ *Client, organization *Orga
25192519
}
25202520

25212521
func createProject(t *testing.T, client *Client, org *Organization) (*Project, func()) {
2522+
return createProjectWithOptions(t, client, org, ProjectCreateOptions{
2523+
Name: randomStringWithoutSpecialChar(t),
2524+
})
2525+
}
2526+
2527+
func createProjectWithOptions(t *testing.T, client *Client, org *Organization, options ProjectCreateOptions) (*Project, func()) {
25222528
var orgCleanup func()
25232529

25242530
if org == nil {
25252531
org, orgCleanup = createOrganization(t, client)
25262532
}
25272533

25282534
ctx := context.Background()
2529-
p, err := client.Projects.Create(ctx, org.Name, ProjectCreateOptions{
2530-
Name: randomStringWithoutSpecialChar(t),
2531-
})
2535+
p, err := client.Projects.Create(ctx, org.Name, options)
25322536
if err != nil {
25332537
t.Fatal(err)
25342538
}

organization.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ type OrganizationUpdateOptions struct {
300300

301301
// Optional: DefaultAgentPoolId default agent pool for workspaces, requires DefaultExecutionMode to be set to `agent`
302302
DefaultAgentPool *AgentPool `jsonapi:"relation,default-agent-pool,omitempty"`
303+
304+
// Optional: StacksEnabled toggles whether stacks are enabled for the organization. This setting
305+
// is considered BETA, SUBJECT TO CHANGE, and likely unavailable to most users.
306+
StacksEnabled *bool `jsonapi:"attr,stacks-enabled,omitempty"`
303307
}
304308

305309
// ReadRunQueueOptions represents the options for showing the queue.

project.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type Projects interface {
3131

3232
// Delete a project.
3333
Delete(ctx context.Context, projectID string) error
34+
35+
// ListTagBindings lists all tag bindings associated with the project.
36+
ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error)
3437
}
3538

3639
// projects implements Projects
@@ -67,6 +70,10 @@ type ProjectListOptions struct {
6770

6871
// Optional: A query string to search projects by names.
6972
Query string `url:"q,omitempty"`
73+
74+
// Optional: A filter string to list projects filtered by key/value tags.
75+
// These are not annotated and therefore not encoded by go-querystring
76+
TagBindings []*TagBinding
7077
}
7178

7279
// ProjectCreateOptions represents the options for creating a project
@@ -82,6 +89,9 @@ type ProjectCreateOptions struct {
8289

8390
// Optional: A description for the project.
8491
Description *string `jsonapi:"attr,description,omitempty"`
92+
93+
// Associated TagBindings of the project.
94+
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
8595
}
8696

8797
// ProjectUpdateOptions represents the options for updating a project
@@ -97,6 +107,10 @@ type ProjectUpdateOptions struct {
97107

98108
// Optional: A description for the project.
99109
Description *string `jsonapi:"attr,description,omitempty"`
110+
111+
// Associated TagBindings of the project. Note that this will replace
112+
// all existing tag bindings.
113+
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
100114
}
101115

102116
// List all projects.
@@ -105,8 +119,13 @@ func (s *projects) List(ctx context.Context, organization string, options *Proje
105119
return nil, ErrInvalidOrg
106120
}
107121

122+
var tagFilters map[string][]string
123+
if options != nil {
124+
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
125+
}
126+
108127
u := fmt.Sprintf("organizations/%s/projects", url.PathEscape(organization))
109-
req, err := s.client.NewRequest("GET", u, options)
128+
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
110129
if err != nil {
111130
return nil, err
112131
}
@@ -166,6 +185,30 @@ func (s *projects) Read(ctx context.Context, projectID string) (*Project, error)
166185
return p, nil
167186
}
168187

188+
func (s *projects) ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error) {
189+
if !validStringID(&projectID) {
190+
return nil, ErrInvalidProjectID
191+
}
192+
193+
u := fmt.Sprintf("projects/%s/tag-bindings", url.PathEscape(projectID))
194+
req, err := s.client.NewRequest("GET", u, nil)
195+
if err != nil {
196+
return nil, err
197+
}
198+
199+
var list struct {
200+
*Pagination
201+
Items []*TagBinding
202+
}
203+
204+
err = req.Do(ctx, &list)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
return list.Items, nil
210+
}
211+
169212
// Update a project by its ID
170213
func (s *projects) Update(ctx context.Context, projectID string, options ProjectUpdateOptions) (*Project, error) {
171214
if !validStringID(&projectID) {

projects_integration_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,53 @@ func TestProjectsList(t *testing.T) {
6262
assert.Nil(t, pl)
6363
assert.EqualError(t, err, ErrInvalidOrg.Error())
6464
})
65+
66+
t.Run("when using a tags filter", func(t *testing.T) {
67+
p1, wTestCleanup1 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
68+
Name: randomStringWithoutSpecialChar(t),
69+
TagBindings: []*TagBinding{
70+
{Key: "key1", Value: "value1"},
71+
{Key: "key2", Value: "value2a"},
72+
},
73+
})
74+
p2, wTestCleanup2 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
75+
Name: randomStringWithoutSpecialChar(t),
76+
TagBindings: []*TagBinding{
77+
{Key: "key2", Value: "value2b"},
78+
{Key: "key3", Value: "value3"},
79+
},
80+
})
81+
t.Cleanup(wTestCleanup1)
82+
t.Cleanup(wTestCleanup2)
83+
84+
// List all the workspaces under the given tag
85+
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
86+
TagBindings: []*TagBinding{
87+
{Key: "key1"},
88+
},
89+
})
90+
assert.NoError(t, err)
91+
assert.Len(t, pl.Items, 1)
92+
assert.Contains(t, pl.Items, p1)
93+
94+
pl2, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
95+
TagBindings: []*TagBinding{
96+
{Key: "key2"},
97+
},
98+
})
99+
assert.NoError(t, err)
100+
assert.Len(t, pl2.Items, 2)
101+
assert.Contains(t, pl2.Items, p1, p2)
102+
103+
pl3, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
104+
TagBindings: []*TagBinding{
105+
{Key: "key2", Value: "value2b"},
106+
},
107+
})
108+
assert.NoError(t, err)
109+
assert.Len(t, pl3.Items, 1)
110+
assert.Contains(t, pl3.Items, p2)
111+
})
65112
}
66113

67114
func TestProjectsRead(t *testing.T) {
@@ -160,12 +207,22 @@ func TestProjectsUpdate(t *testing.T) {
160207
kAfter, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
161208
Name: String("new project name"),
162209
Description: String("updated description"),
210+
TagBindings: []*TagBinding{
211+
{Key: "foo", Value: "bar"},
212+
},
163213
})
164214
require.NoError(t, err)
165215

166216
assert.Equal(t, kBefore.ID, kAfter.ID)
167217
assert.NotEqual(t, kBefore.Name, kAfter.Name)
168218
assert.NotEqual(t, kBefore.Description, kAfter.Description)
219+
220+
bindings, err := client.Projects.ListTagBindings(ctx, kAfter.ID)
221+
require.NoError(t, err)
222+
223+
assert.Len(t, bindings, 1)
224+
assert.Equal(t, "foo", bindings[0].Key)
225+
assert.Equal(t, "bar", bindings[0].Value)
169226
})
170227

171228
t.Run("when updating with invalid name", func(t *testing.T) {

tag.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
package tfe
55

6+
import "fmt"
7+
68
type TagList struct {
79
*Pagination
810
Items []*Tag
@@ -13,3 +15,23 @@ type Tag struct {
1315
ID string `jsonapi:"primary,tags"`
1416
Name string `jsonapi:"attr,name,omitempty"`
1517
}
18+
19+
type TagBinding struct {
20+
ID string `jsonapi:"primary,tag-bindings"`
21+
Key string `jsonapi:"attr,key"`
22+
Value string `jsonapi:"attr,value,omitempty"`
23+
}
24+
25+
func encodeTagFiltersAsParams(filters []*TagBinding) map[string][]string {
26+
if len(filters) == 0 {
27+
return nil
28+
}
29+
30+
var tagFilter = make(map[string][]string, len(filters))
31+
for index, tag := range filters {
32+
tagFilter[fmt.Sprintf("filter[tagged][%d][key]", index)] = []string{tag.Key}
33+
tagFilter[fmt.Sprintf("filter[tagged][%d][value]", index)] = []string{tag.Value}
34+
}
35+
36+
return tagFilter
37+
}

workspace.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ type Workspaces interface {
131131
// DeleteDataRetentionPolicy deletes a workspace's data retention policy
132132
// **Note: This functionality is only available in Terraform Enterprise.**
133133
DeleteDataRetentionPolicy(ctx context.Context, workspaceID string) error
134+
135+
// ListTagBindings lists all tag bindings associated with the workspace.
136+
ListTagBindings(ctx context.Context, workspaceID string) ([]*TagBinding, error)
134137
}
135138

136139
// workspaces implements Workspaces.
@@ -208,6 +211,7 @@ type Workspace struct {
208211
CurrentConfigurationVersion *ConfigurationVersion `jsonapi:"relation,current-configuration-version,omitempty"`
209212
LockedBy *LockedByChoice `jsonapi:"polyrelation,locked-by"`
210213
Variables []*Variable `jsonapi:"relation,vars"`
214+
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings"`
211215

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

336+
// Optional: A filter string to list workspaces filtered by key/value tags.
337+
// These are not annotated and therefore not encoded by go-querystring
338+
TagBindings []*TagBinding
339+
332340
// Optional: A list of relations to include. See available resources https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#available-related-resources
333341
Include []WSIncludeOpt `url:"include,omitempty"`
334342

@@ -471,6 +479,9 @@ type WorkspaceCreateOptions struct {
471479
// Associated Project with the workspace. If not provided, default project
472480
// of the organization will be assigned to the workspace.
473481
Project *Project `jsonapi:"relation,project,omitempty"`
482+
483+
// Associated TagBindings of the workspace.
484+
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
474485
}
475486

476487
// TODO: move this struct out. VCSRepoOptions is used by workspaces, policy sets, and registry modules
@@ -610,6 +621,10 @@ type WorkspaceUpdateOptions struct {
610621
// Associated Project with the workspace. If not provided, default project
611622
// of the organization will be assigned to the workspace
612623
Project *Project `jsonapi:"relation,project,omitempty"`
624+
625+
// Associated TagBindings of the project. Note that this will replace
626+
// all existing tag bindings.
627+
TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"`
613628
}
614629

615630
// WorkspaceLockOptions represents the options for locking a workspace.
@@ -700,8 +715,14 @@ func (s *workspaces) List(ctx context.Context, organization string, options *Wor
700715
return nil, err
701716
}
702717

718+
var tagFilters map[string][]string
719+
if options != nil {
720+
tagFilters = encodeTagFiltersAsParams(options.TagBindings)
721+
}
722+
723+
// Encode parameters that cannot be encoded by go-querystring
703724
u := fmt.Sprintf("organizations/%s/workspaces", url.PathEscape(organization))
704-
req, err := s.client.NewRequest("GET", u, options)
725+
req, err := s.client.NewRequestWithAdditionalQueryParams("GET", u, options, tagFilters)
705726
if err != nil {
706727
return nil, err
707728
}
@@ -715,6 +736,30 @@ func (s *workspaces) List(ctx context.Context, organization string, options *Wor
715736
return wl, nil
716737
}
717738

739+
func (s *workspaces) ListTagBindings(ctx context.Context, workspaceID string) ([]*TagBinding, error) {
740+
if !validStringID(&workspaceID) {
741+
return nil, ErrInvalidWorkspaceID
742+
}
743+
744+
u := fmt.Sprintf("workspaces/%s/tag-bindings", url.PathEscape(workspaceID))
745+
req, err := s.client.NewRequest("GET", u, nil)
746+
if err != nil {
747+
return nil, err
748+
}
749+
750+
var list struct {
751+
*Pagination
752+
Items []*TagBinding
753+
}
754+
755+
err = req.Do(ctx, &list)
756+
if err != nil {
757+
return nil, err
758+
}
759+
760+
return list.Items, nil
761+
}
762+
718763
// Create is used to create a new workspace.
719764
func (s *workspaces) Create(ctx context.Context, organization string, options WorkspaceCreateOptions) (*Workspace, error) {
720765
if !validStringID(&organization) {
@@ -1436,7 +1481,7 @@ func (o WorkspaceCreateOptions) valid() error {
14361481
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
14371482
return ErrRequiredAgentPoolID
14381483
}
1439-
if o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 &&
1484+
if len(o.TriggerPrefixes) > 0 &&
14401485
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
14411486
return ErrUnsupportedBothTriggerPatternsAndPrefixes
14421487
}
@@ -1466,7 +1511,7 @@ func (o WorkspaceUpdateOptions) valid() error {
14661511
if o.AgentPoolID == nil && (o.ExecutionMode != nil && *o.ExecutionMode == "agent") {
14671512
return ErrRequiredAgentPoolID
14681513
}
1469-
if o.TriggerPrefixes != nil && len(o.TriggerPrefixes) > 0 &&
1514+
if len(o.TriggerPrefixes) > 0 &&
14701515
o.TriggerPatterns != nil && len(o.TriggerPatterns) > 0 {
14711516
return ErrUnsupportedBothTriggerPatternsAndPrefixes
14721517
}

0 commit comments

Comments
 (0)