Skip to content

Commit

Permalink
Merge pull request #987 from hashicorp/TF-20596-go-tfe-go-tfe-support…
Browse files Browse the repository at this point in the history
…-for-tags

Filtering by and updating tag-bindings
  • Loading branch information
brandonc authored Oct 17, 2024
2 parents d344210 + 89f0127 commit 9359d0a
Show file tree
Hide file tree
Showing 10 changed files with 282 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 [#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)

# 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
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.

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
45 changes: 44 additions & 1 deletion project.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type Projects interface {

// Delete a project.
Delete(ctx context.Context, projectID string) error

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

// projects implements Projects
Expand Down Expand Up @@ -67,6 +70,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 +89,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 @@ -97,6 +107,10 @@ type ProjectUpdateOptions struct {

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

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

// List all projects.
Expand All @@ -105,8 +119,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 Expand Up @@ -166,6 +185,30 @@ func (s *projects) Read(ctx context.Context, projectID string) (*Project, error)
return p, nil
}

func (s *projects) ListTagBindings(ctx context.Context, projectID string) ([]*TagBinding, error) {
if !validStringID(&projectID) {
return nil, ErrInvalidProjectID
}

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

var list struct {
*Pagination
Items []*TagBinding
}

err = req.Do(ctx, &list)
if err != nil {
return nil, err
}

return list.Items, nil
}

// Update a project by its ID
func (s *projects) Update(ctx context.Context, projectID string, options ProjectUpdateOptions) (*Project, error) {
if !validStringID(&projectID) {
Expand Down
61 changes: 61 additions & 0 deletions projects_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,55 @@ 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) {
skipUnlessBeta(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 Expand Up @@ -160,12 +209,24 @@ func TestProjectsUpdate(t *testing.T) {
kAfter, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{
Name: String("new project name"),
Description: String("updated description"),
TagBindings: []*TagBinding{
{Key: "foo", Value: "bar"},
},
})
require.NoError(t, err)

assert.Equal(t, kBefore.ID, kAfter.ID)
assert.NotEqual(t, kBefore.Name, kAfter.Name)
assert.NotEqual(t, kBefore.Description, kAfter.Description)

if betaFeaturesEnabled() {
bindings, err := client.Projects.ListTagBindings(ctx, kAfter.ID)
require.NoError(t, err)

assert.Len(t, bindings, 1)
assert.Equal(t, "foo", bindings[0].Key)
assert.Equal(t, "bar", bindings[0].Value)
}
})

t.Run("when updating with invalid name", func(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 = make(map[string][]string, len(filters))
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
}
Loading

0 comments on commit 9359d0a

Please sign in to comment.