Skip to content

Commit 9359d0a

Browse files
authored
Merge pull request #987 from hashicorp/TF-20596-go-tfe-go-tfe-support-for-tags
Filtering by and updating tag-bindings
2 parents d344210 + 89f0127 commit 9359d0a

10 files changed

+282
-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
}

mocks/project_mocks.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mocks/workspace_mocks.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,55 @@ 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+
skipUnlessBeta(t)
68+
69+
p1, wTestCleanup1 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
70+
Name: randomStringWithoutSpecialChar(t),
71+
TagBindings: []*TagBinding{
72+
{Key: "key1", Value: "value1"},
73+
{Key: "key2", Value: "value2a"},
74+
},
75+
})
76+
p2, wTestCleanup2 := createProjectWithOptions(t, client, orgTest, ProjectCreateOptions{
77+
Name: randomStringWithoutSpecialChar(t),
78+
TagBindings: []*TagBinding{
79+
{Key: "key2", Value: "value2b"},
80+
{Key: "key3", Value: "value3"},
81+
},
82+
})
83+
t.Cleanup(wTestCleanup1)
84+
t.Cleanup(wTestCleanup2)
85+
86+
// List all the workspaces under the given tag
87+
pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
88+
TagBindings: []*TagBinding{
89+
{Key: "key1"},
90+
},
91+
})
92+
assert.NoError(t, err)
93+
assert.Len(t, pl.Items, 1)
94+
assert.Contains(t, pl.Items, p1)
95+
96+
pl2, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
97+
TagBindings: []*TagBinding{
98+
{Key: "key2"},
99+
},
100+
})
101+
assert.NoError(t, err)
102+
assert.Len(t, pl2.Items, 2)
103+
assert.Contains(t, pl2.Items, p1, p2)
104+
105+
pl3, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{
106+
TagBindings: []*TagBinding{
107+
{Key: "key2", Value: "value2b"},
108+
},
109+
})
110+
assert.NoError(t, err)
111+
assert.Len(t, pl3.Items, 1)
112+
assert.Contains(t, pl3.Items, p2)
113+
})
65114
}
66115

67116
func TestProjectsRead(t *testing.T) {
@@ -160,12 +209,24 @@ func TestProjectsUpdate(t *testing.T) {
160209
kAfter, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
161210
Name: String("new project name"),
162211
Description: String("updated description"),
212+
TagBindings: []*TagBinding{
213+
{Key: "foo", Value: "bar"},
214+
},
163215
})
164216
require.NoError(t, err)
165217

166218
assert.Equal(t, kBefore.ID, kAfter.ID)
167219
assert.NotEqual(t, kBefore.Name, kAfter.Name)
168220
assert.NotEqual(t, kBefore.Description, kAfter.Description)
221+
222+
if betaFeaturesEnabled() {
223+
bindings, err := client.Projects.ListTagBindings(ctx, kAfter.ID)
224+
require.NoError(t, err)
225+
226+
assert.Len(t, bindings, 1)
227+
assert.Equal(t, "foo", bindings[0].Key)
228+
assert.Equal(t, "bar", bindings[0].Value)
229+
}
169230
})
170231

171232
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+
}

0 commit comments

Comments
 (0)