Skip to content

Commit a0264fe

Browse files
authored
Merge pull request #816 from hashicorp/brandonc/poly-poc
Adds polymorphic relationships for Workspaces, Tokens, and RunTriggers
2 parents b68fdd6 + c618384 commit a0264fe

14 files changed

+127
-30
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
* Allow managing workspace and organization data retention policies by @mwudka [#801](https://github.com/hashicorp/go-tfe/pull/817)
66

7+
## Deprecations
8+
* 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)
9+
10+
## Features
11+
* Adds `LockedBy` relationship field to `Workspace` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
12+
* Adds `CreatedBy` relationship field to `TeamToken`, `UserToken`, and `OrganizationToken` by @brandonc [#816](https://github.com/hashicorp/go-tfe/pull/816)
13+
714
# v1.40.0
815

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

1825
# v1.39.2
1926

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/hashicorp/go-slug v0.13.3
1111
github.com/hashicorp/go-uuid v1.0.3
1212
github.com/hashicorp/go-version v1.6.0
13-
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
13+
github.com/hashicorp/jsonapi v1.2.0
1414
github.com/stretchr/testify v1.8.4
1515
golang.org/x/sync v0.5.0
1616
golang.org/x/time v0.5.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
1818
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
1919
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
2020
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
21-
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0=
22-
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
21+
github.com/hashicorp/jsonapi v1.2.0 h1:ezDCzOFsKTL+KxVQuA1rNxkIGTvZph1rNu8kT5A8trI=
22+
github.com/hashicorp/jsonapi v1.2.0/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=
2323
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2424
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2525
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

helper_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"os/exec"
2424
"path/filepath"
25+
"reflect"
2526
"runtime"
2627
"strings"
2728
"testing"
@@ -2851,6 +2852,53 @@ func betaFeaturesEnabled() bool {
28512852
return os.Getenv("ENABLE_BETA") == "1"
28522853
}
28532854

2855+
// isEmpty gets whether the specified object is considered empty or not.
2856+
func isEmpty(object interface{}) bool {
2857+
// get nil case out of the way
2858+
if object == nil {
2859+
return true
2860+
}
2861+
2862+
objValue := reflect.ValueOf(object)
2863+
2864+
switch objValue.Kind() {
2865+
// collection types are empty when they have no element
2866+
case reflect.Chan, reflect.Map, reflect.Slice:
2867+
return objValue.Len() == 0
2868+
// pointers are empty if nil or if the value they point to is empty
2869+
case reflect.Ptr:
2870+
if objValue.IsNil() {
2871+
return true
2872+
}
2873+
deref := objValue.Elem().Interface()
2874+
return isEmpty(deref)
2875+
// for all other types, compare against the zero value
2876+
// array types are empty when they match their zero-initialized state
2877+
default:
2878+
zero := reflect.Zero(objValue.Type())
2879+
return reflect.DeepEqual(object, zero.Interface())
2880+
}
2881+
}
2882+
2883+
// requireExactlyOneNotEmpty accepts any number of values and calls t.Fatal if
2884+
// less or more than one is empty.
2885+
func requireExactlyOneNotEmpty(t *testing.T, v ...any) {
2886+
if len(v) == 0 {
2887+
t.Fatal("Expected some values for requireExactlyOneNotEmpty, but received none")
2888+
}
2889+
2890+
empty := 0
2891+
for _, value := range v {
2892+
if isEmpty(value) {
2893+
empty += 1
2894+
}
2895+
}
2896+
2897+
if empty != len(v)-1 {
2898+
t.Fatalf("Expected exactly one value to not be empty, but found %d empty values", empty)
2899+
}
2900+
}
2901+
28542902
// Useless key but enough to pass validation in the API
28552903
const testGpgArmor string = `
28562904
-----BEGIN PGP PUBLIC KEY BLOCK-----

organization_token.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ type organizationTokens struct {
3939

4040
// OrganizationToken represents a Terraform Enterprise organization token.
4141
type OrganizationToken struct {
42-
ID string `jsonapi:"primary,authentication-tokens"`
43-
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
44-
Description string `jsonapi:"attr,description"`
45-
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
46-
Token string `jsonapi:"attr,token"`
47-
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
42+
ID string `jsonapi:"primary,authentication-tokens"`
43+
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
44+
Description string `jsonapi:"attr,description"`
45+
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
46+
Token string `jsonapi:"attr,token"`
47+
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
48+
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
4849
}
4950

5051
// OrganizationTokenCreateOptions contains the options for creating an organization token.

organization_token_integration_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func TestOrganizationTokensCreate(t *testing.T) {
2424
ot, err := client.OrganizationTokens.Create(ctx, orgTest.Name)
2525
require.NoError(t, err)
2626
require.NotEmpty(t, ot.Token)
27+
requireExactlyOneNotEmpty(t, ot.CreatedBy.Organization, ot.CreatedBy.Team, ot.CreatedBy.User)
2728
tkToken = ot.Token
2829
})
2930

run_trigger.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ type RunTriggerList struct {
4343
Items []*RunTrigger
4444
}
4545

46+
// SourceableChoice is a choice type struct that represents the possible values
47+
// within a polymorphic relation. If a value is available, exactly one field
48+
// will be non-nil.
49+
type SourceableChoice struct {
50+
Workspace *Workspace
51+
}
52+
4653
// RunTrigger represents a run trigger.
4754
type RunTrigger struct {
4855
ID string `jsonapi:"primary,run-triggers"`
4956
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
5057
SourceableName string `jsonapi:"attr,sourceable-name"`
5158
WorkspaceName string `jsonapi:"attr,workspace-name"`
52-
53-
// Relations
54-
// TODO: this will eventually need to be polymorphic
55-
Sourceable *Workspace `jsonapi:"relation,sourceable"`
56-
Workspace *Workspace `jsonapi:"relation,workspace"`
59+
// DEPRECATED. The sourceable field is polymorphic. Use SourceableChoice instead.
60+
Sourceable *Workspace `jsonapi:"relation,sourceable"`
61+
SourceableChoice *SourceableChoice `jsonapi:"polyrelation,sourceable"`
62+
Workspace *Workspace `jsonapi:"relation,workspace"`
5763
}
5864

5965
// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/run-triggers#query-parameters

run_trigger_integration_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ func TestRunTriggerList(t *testing.T) {
129129
require.NoError(t, err)
130130
require.NotEmpty(t, rtl.Items)
131131
require.NotNil(t, rtl.Items[0].Sourceable)
132-
assert.NotEmpty(t, rtl.Items[0].Sourceable.Name)
132+
assert.NotEmpty(t, rtl.Items[0].Sourceable)
133+
assert.NotNil(t, rtl.Items[0].SourceableChoice.Workspace)
134+
assert.NotEmpty(t, rtl.Items[0].SourceableChoice.Workspace)
133135
})
134136

135137
t.Run("with a RunTriggerType that does not return included data", func(t *testing.T) {

team_token.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ type teamTokens struct {
3939

4040
// TeamToken represents a Terraform Enterprise team token.
4141
type TeamToken struct {
42-
ID string `jsonapi:"primary,authentication-tokens"`
43-
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
44-
Description string `jsonapi:"attr,description"`
45-
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
46-
Token string `jsonapi:"attr,token"`
47-
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
42+
ID string `jsonapi:"primary,authentication-tokens"`
43+
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
44+
Description string `jsonapi:"attr,description"`
45+
LastUsedAt time.Time `jsonapi:"attr,last-used-at,iso8601"`
46+
Token string `jsonapi:"attr,token"`
47+
ExpiredAt time.Time `jsonapi:"attr,expired-at,iso8601"`
48+
CreatedBy *CreatedByChoice `jsonapi:"polyrelation,created-by"`
4849
}
4950

5051
// TeamTokenCreateOptions contains the options for creating a team token.

team_token_integration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func TestTeamTokensCreate(t *testing.T) {
2424
tt, err := client.TeamTokens.Create(ctx, tmTest.ID)
2525
require.NoError(t, err)
2626
require.NotEmpty(t, tt.Token)
27+
require.NotEmpty(t, tt.CreatedBy)
28+
requireExactlyOneNotEmpty(t, tt.CreatedBy.Organization, tt.CreatedBy.Team, tt.CreatedBy.User)
2729
tmToken = tt.Token
2830
})
2931

0 commit comments

Comments
 (0)