Skip to content

Commit

Permalink
Implement polymorphic relationship fields
Browse files Browse the repository at this point in the history
`LockedBy` and `CreatedBy` are new fields, but deprecates `Sourceable` on
the `RunTrigger` model
  • Loading branch information
brandonc committed Nov 15, 2023
1 parent 51a9039 commit a78dac9
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 29 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# 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)

## Bug Fixes
* Removed unused field `AgentPoolID` from the Workspace model. (Callers should be using the `AgentPool` relation instead) by @brandonc [#815](https://github.com/hashicorp/go-tfe/pull/815)

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

## Bug Fixes
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 @@ -2850,6 +2851,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

0 comments on commit a78dac9

Please sign in to comment.