Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds polymorphic relationships for Workspaces, Tokens, and RunTriggers #816

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# UNRELEASED

## Deprecations
* The `Sourceable` field has been deprecated on `RunTrigger`. Instead, use `SourceableChoice` to locate the non-empty field representing the actual sourceable value by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)

## Features
* Adds `LockedBy` relationship field to `Workspace` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
* Adds `CreatedBy` relationship field to `TeamToken`, `UserToken`, and `OrganizationToken` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)

# v1.40.0

## Bug Fixes
Expand All @@ -9,7 +16,7 @@
* Add organization scope field for oauth clients by @Netra2104 [#812](https://github.com/hashicorp/go-tfe/pull/812)
* Added BETA support for including `projects` relationship to oauth_client on create by @Netra2104 [#806](https://github.com/hashicorp/go-tfe/pull/806)
* Added BETA method `AddProjects` and `RemoveProjects` for attaching/detaching oauth_client to projects by Netra2104 [#806](https://github.com/hashicorp/go-tfe/pull/806)
* Adds a missing interface `WorkspaceResources` and the `List` method by @stefan-kiss [Issue#754](https://github.com/hashicorp/go-tfe/issues/754)
* Adds a missing interface `WorkspaceResources` and the `List` method by @stefan-kiss [Issue#754](https://github.com/hashicorp/go-tfe/issues/754)

# v1.39.2

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/hashicorp/go-slug v0.13.2
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
github.com/hashicorp/jsonapi v1.2.0
github.com/stretchr/testify v1.8.4
golang.org/x/sync v0.5.0
golang.org/x/time v0.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0=
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
github.com/hashicorp/jsonapi v1.2.0 h1:ezDCzOFsKTL+KxVQuA1rNxkIGTvZph1rNu8kT5A8trI=
github.com/hashicorp/jsonapi v1.2.0/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
Expand Down
48 changes: 48 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
Expand Down Expand Up @@ -2851,6 +2852,53 @@ func betaFeaturesEnabled() bool {
return os.Getenv("ENABLE_BETA") == "1"
}

// isEmpty gets whether the specified object is considered empty or not.
func isEmpty(object interface{}) bool {
// get nil case out of the way
if object == nil {
return true
}

objValue := reflect.ValueOf(object)

switch objValue.Kind() {
// collection types are empty when they have no element
case reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
// pointers are empty if nil or if the value they point to is empty
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
// for all other types, compare against the zero value
// array types are empty when they match their zero-initialized state
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
}
}

// requireExactlyOneNotEmpty accepts any number of values and calls t.Fatal if
// less or more than one is empty.
func requireExactlyOneNotEmpty(t *testing.T, v ...any) {
if len(v) == 0 {
t.Fatal("Expected some values for requireExactlyOneNotEmpty, but received none")
}

empty := 0
for _, value := range v {
if isEmpty(value) {
empty += 1
}
}

if empty != len(v)-1 {
t.Fatalf("Expected exactly one value to not be empty, but found %d empty values", empty)
}
}

// Useless key but enough to pass validation in the API
const testGpgArmor string = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
Expand Down
13 changes: 7 additions & 6 deletions organization_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ type organizationTokens struct {

// OrganizationToken represents a Terraform Enterprise organization token.
type OrganizationToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
}

// OrganizationTokenCreateOptions contains the options for creating an organization token.
Expand Down
1 change: 1 addition & 0 deletions organization_token_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestOrganizationTokensCreate(t *testing.T) {
ot, err := client.OrganizationTokens.Create(ctx, orgTest.Name)
require.NoError(t, err)
require.NotEmpty(t, ot.Token)
requireExactlyOneNotEmpty(t, ot.CreatedBy.Organization, ot.CreatedBy.Team, ot.CreatedBy.User)
tkToken = ot.Token
})

Expand Down
16 changes: 11 additions & 5 deletions run_trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,23 @@ type RunTriggerList struct {
Items []*RunTrigger
}

// SourceableChoice 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.
type SourceableChoice struct {
Workspace *Workspace
}

// RunTrigger represents a run trigger.
type RunTrigger struct {
ID string `jsonapi:"primary,run-triggers"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
SourceableName string `jsonapi:"attr,sourceable-name"`
WorkspaceName string `jsonapi:"attr,workspace-name"`

// Relations
// TODO: this will eventually need to be polymorphic
Sourceable *Workspace `jsonapi:"relation,sourceable"`
Workspace *Workspace `jsonapi:"relation,workspace"`
// DEPRECATED. The sourceable field is polymorphic. Use SourceableChoice instead.
Sourceable *Workspace `jsonapi:"relation,sourceable"`
SourceableChoice *SourceableChoice `jsonapi:"polyrelation,sourceable"`
Workspace *Workspace `jsonapi:"relation,workspace"`
}

// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#query-parameters
Expand Down
4 changes: 3 additions & 1 deletion run_trigger_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ func TestRunTriggerList(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, rtl.Items)
require.NotNil(t, rtl.Items[0].Sourceable)
assert.NotEmpty(t, rtl.Items[0].Sourceable.Name)
assert.NotEmpty(t, rtl.Items[0].Sourceable)
assert.NotNil(t, rtl.Items[0].SourceableChoice.Workspace)
assert.NotEmpty(t, rtl.Items[0].SourceableChoice.Workspace)
})

t.Run("with a RunTriggerType that does not return included data", func(t *testing.T) {
Expand Down
13 changes: 7 additions & 6 deletions team_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ type teamTokens struct {

// TeamToken represents a Terraform Enterprise team token.
type TeamToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
}

// TeamTokenCreateOptions contains the options for creating a team token.
Expand Down
2 changes: 2 additions & 0 deletions team_token_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func TestTeamTokensCreate(t *testing.T) {
tt, err := client.TeamTokens.Create(ctx, tmTest.ID)
require.NoError(t, err)
require.NotEmpty(t, tt.Token)
require.NotEmpty(t, tt.CreatedBy)
requireExactlyOneNotEmpty(t, tt.CreatedBy.Organization, tt.CreatedBy.Team, tt.CreatedBy.User)
tmToken = tt.Token
})

Expand Down
22 changes: 16 additions & 6 deletions user_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,24 @@ type UserTokenList struct {
Items []*UserToken
}

// CreatedByChoice 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.
type CreatedByChoice struct {
Organization *Organization
Team *Team
User *User
}

// UserToken represents a Terraform Enterprise user token.
type UserToken struct {
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
ID string `jsonapi:"primary,authentication-tokens"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Description string `jsonapi:"attr,description"`
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
Token string `jsonapi:"attr,token"`
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
}

// UserTokenCreateOptions contains the options for creating a user token.
Expand Down
7 changes: 5 additions & 2 deletions user_token_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ package tfe
import (
"context"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestUserTokens_List tests listing user tokens
Expand Down Expand Up @@ -122,6 +123,8 @@ func TestUserTokens_Read(t *testing.T) {
// object. Empty that out for comparison
token.Token = ""
assert.Equal(t, token, to)

requireExactlyOneNotEmpty(t, token.CreatedBy.Organization, token.CreatedBy.Team, token.CreatedBy.User)
})
}

Expand Down
10 changes: 10 additions & 0 deletions workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ type WorkspaceList struct {
Items []*Workspace
}

// 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.
type LockedByChoice struct {
Run *Run
User *User
Team *Team
}

// Workspace represents a Terraform Enterprise workspace.
type Workspace struct {
ID string `jsonapi:"primary,workspaces"`
Expand Down Expand Up @@ -164,6 +173,7 @@ type Workspace struct {
Project *Project `jsonapi:"relation,project"`
Tags []*Tag `jsonapi:"relation,tags"`
CurrentConfigurationVersion *ConfigurationVersion `jsonapi:"relation,current-configuration-version,omitempty"`
LockedBy *LockedByChoice `jsonapi:"polyrelation,locked-by"`

// Links
Links map[string]interface{} `jsonapi:"links,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions workspace_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1874,9 +1874,15 @@ func TestWorkspacesLock(t *testing.T) {
t.Cleanup(wTestCleanup)

t.Run("with valid options", func(t *testing.T) {
require.Empty(t, wTest.LockedBy)

w, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{})
require.NoError(t, err)
assert.True(t, w.Locked)

require.NoError(t, err)
require.NotEmpty(t, w.LockedBy)
requireExactlyOneNotEmpty(t, w.LockedBy.Run, w.LockedBy.Team, w.LockedBy.User)
})

t.Run("when workspace is already locked", func(t *testing.T) {
Expand Down
Loading