diff --git a/backend/remote/backend_apply.go b/backend/remote/backend_apply.go index ef5b1511c..5db412baa 100644 --- a/backend/remote/backend_apply.go +++ b/backend/remote/backend_apply.go @@ -8,6 +8,7 @@ import ( "log" tfe "github.com/hashicorp/go-tfe" + version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" @@ -67,14 +68,6 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati )) } - if op.Targets != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Resource targeting is currently not supported", - `The "remote" backend does not support resource targeting at this time.`, - )) - } - if b.hasExplicitVariableValues(op) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -102,6 +95,26 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati )) } + if len(op.Targets) != 0 { + // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, + // so if there's an error when parsing the RemoteAPIVersion, it's handled as + // equivalent to an API version < 2.3. + currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) + desiredAPIVersion, _ := version.NewVersion("2.3") + + if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource targeting is not supported", + fmt.Sprintf( + `The host %s does not support the -target option for `+ + `remote plans.`, + b.hostname, + ), + )) + } + } + // Return if there are any errors. if diags.HasErrors() { return nil, diags.Err() diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index a8d2ef70c..87a1cbc72 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" @@ -278,16 +279,23 @@ func TestRemote_applyWithTarget(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { - t.Fatal("expected apply operation to fail") + if run.Result != backend.OperationSuccess { + t.Fatal("expected apply operation to succeed") } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") + if run.PlanEmpty { + t.Fatalf("expected plan to be non-empty") } - errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() - if !strings.Contains(errOutput, "targeting is currently not supported") { - t.Fatalf("expected a targeting error, got: %v", errOutput) + // We should find a run inside the mock client that has the same + // target address we requested above. + runsAPI := b.client.Runs.(*mockRuns) + if got, want := len(runsAPI.runs), 1; got != want { + t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) + } + for _, run := range runsAPI.runs { + if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { + t.Errorf("wrong TargetAddrs in the created run\n%s", diff) + } } } diff --git a/backend/remote/backend_common.go b/backend/remote/backend_common.go index c92b80e92..b83b9d829 100644 --- a/backend/remote/backend_common.go +++ b/backend/remote/backend_common.go @@ -316,6 +316,10 @@ func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Op b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n")) } continue + case "skipped_due_to_targeting": // TEMP: not available in the go-tfe library yet; will update this to be tfe.CostEstimateSkippedDueToTargeting once that's available. + b.CLI.Output("Not available for this plan, because it was created with the -target option.") + b.CLI.Output("\n------------------------------------------------------------------------") + return nil case tfe.CostEstimateErrored: return fmt.Errorf(msgPrefix + " errored.") case tfe.CostEstimateCanceled: diff --git a/backend/remote/backend_context.go b/backend/remote/backend_context.go index 9461cb54a..13202f547 100644 --- a/backend/remote/backend_context.go +++ b/backend/remote/backend_context.go @@ -30,23 +30,17 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu } // Get the remote workspace name. - workspace := op.Workspace - switch { - case op.Workspace == backend.DefaultStateName: - workspace = b.workspace - case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix): - workspace = b.prefix + op.Workspace - } + remoteWorkspaceName := b.getRemoteWorkspaceName(op.Workspace) // Get the latest state. - log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", workspace) + log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", remoteWorkspaceName) stateMgr, err := b.StateMgr(op.Workspace) if err != nil { diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) return nil, nil, diags } - log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", workspace) + log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", remoteWorkspaceName) if err := op.StateLocker.Lock(stateMgr, op.Type.String()); err != nil { diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err)) return nil, nil, diags @@ -63,7 +57,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu } }() - log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", workspace) + log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", remoteWorkspaceName) if err := stateMgr.RefreshState(); err != nil { diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) return nil, nil, diags @@ -83,7 +77,7 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu // Load the latest state. If we enter contextFromPlanFile below then the // state snapshot in the plan file must match this, or else it'll return // error diagnostics. - log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", workspace) + log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", remoteWorkspaceName) opts.State = stateMgr.State() log.Printf("[TRACE] backend/remote: loading configuration for the current working directory") @@ -94,11 +88,17 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu } opts.Config = config - log.Printf("[TRACE] backend/remote: retrieving variables from workspace %q", workspace) - tfeVariables, err := b.client.Variables.List(context.Background(), tfe.VariableListOptions{ - Organization: tfe.String(b.organization), - Workspace: tfe.String(workspace), - }) + // The underlying API expects us to use the opaque workspace id to request + // variables, so we'll need to look that up using our organization name + // and workspace name. + remoteWorkspaceID, err := b.getRemoteWorkspaceID(context.Background(), op.Workspace) + if err != nil { + diags = diags.Append(errwrap.Wrapf("Error finding remote workspace: {{err}}", err)) + return nil, nil, diags + } + + log.Printf("[TRACE] backend/remote: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID) + tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, tfe.VariableListOptions{}) if err != nil && err != tfe.ErrResourceNotFound { diags = diags.Append(errwrap.Wrapf("Error loading variables: {{err}}", err)) return nil, nil, diags @@ -142,6 +142,32 @@ func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Fu return tfCtx, stateMgr, diags } +func (b *Remote) getRemoteWorkspaceName(localWorkspaceName string) string { + switch { + case localWorkspaceName == backend.DefaultStateName: + // The default workspace name is a special case, for when the backend + // is configured to with to an exact remote workspace rather than with + // a remote workspace _prefix_. + return b.workspace + case b.prefix != "" && !strings.HasPrefix(localWorkspaceName, b.prefix): + return b.prefix + localWorkspaceName + default: + return localWorkspaceName + } +} + +func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) { + remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName) + + log.Printf("[TRACE] backend/remote: looking up workspace id for %s/%s", b.organization, remoteWorkspaceName) + remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName) + if err != nil { + return "", err + } + + return remoteWorkspace.ID, nil +} + func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues { ret := make(terraform.InputValues, len(decls)) diff --git a/backend/remote/backend_context_test.go b/backend/remote/backend_context_test.go index 03de0f427..a7fa5e713 100644 --- a/backend/remote/backend_context_test.go +++ b/backend/remote/backend_context_test.go @@ -1,6 +1,7 @@ package remote import ( + "context" "testing" tfe "github.com/hashicorp/go-tfe" @@ -176,6 +177,11 @@ func TestRemoteContextWithVars(t *testing.T) { _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) defer configCleanup() + workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + op := &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, @@ -187,12 +193,7 @@ func TestRemoteContextWithVars(t *testing.T) { key := "key" v.Key = &key } - if v.Workspace == nil { - v.Workspace = &tfe.Workspace{ - Name: b.workspace, - } - } - b.client.Variables.Create(nil, *v) + b.client.Variables.Create(nil, workspaceID, *v) _, _, diags := b.Context(op) diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index 42b51fd26..9b9b51126 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -696,6 +696,11 @@ type mockRuns struct { client *mockClient runs map[string]*tfe.Run workspaces map[string][]*tfe.Run + + // If modifyNewRun is non-nil, the create method will call it just before + // saving a new run in the runs map, so that a calling test can mimic + // side-effects that a real server might apply in certain situations. + modifyNewRun func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) } func newMockRuns(client *mockClient) *mockRuns { @@ -757,6 +762,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t Permissions: &tfe.RunPermissions{}, Plan: p, Status: tfe.RunPending, + TargetAddrs: options.TargetAddrs, + } + + if options.Message != nil { + r.Message = *options.Message } if pc != nil { @@ -775,6 +785,12 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t w.CurrentRun = r } + if m.modifyNewRun != nil { + // caller-provided callback may modify the run in-place to mimic + // side-effects that a real server might take in some situations. + m.modifyNewRun(m.client, options, r) + } + m.runs[r.ID] = r m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) @@ -952,6 +968,8 @@ type mockVariables struct { workspaces map[string]*tfe.VariableList } +var _ tfe.Variables = (*mockVariables)(nil) + func newMockVariables(client *mockClient) *mockVariables { return &mockVariables{ client: client, @@ -959,12 +977,12 @@ func newMockVariables(client *mockClient) *mockVariables { } } -func (m *mockVariables) List(ctx context.Context, options tfe.VariableListOptions) (*tfe.VariableList, error) { - vl := m.workspaces[*options.Workspace] +func (m *mockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) { + vl := m.workspaces[workspaceID] return vl, nil } -func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOptions) (*tfe.Variable, error) { +func (m *mockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { v := &tfe.Variable{ ID: generateID("var-"), Key: *options.Key, @@ -980,7 +998,7 @@ func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOp v.Sensitive = *options.Sensitive } - workspace := options.Workspace.Name + workspace := workspaceID if m.workspaces[workspace] == nil { m.workspaces[workspace] = &tfe.VariableList{} @@ -992,15 +1010,15 @@ func (m *mockVariables) Create(ctx context.Context, options tfe.VariableCreateOp return v, nil } -func (m *mockVariables) Read(ctx context.Context, variableID string) (*tfe.Variable, error) { +func (m *mockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { panic("not implemented") } -func (m *mockVariables) Update(ctx context.Context, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { +func (m *mockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { panic("not implemented") } -func (m *mockVariables) Delete(ctx context.Context, variableID string) error { +func (m *mockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { panic("not implemented") } diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go index c83d91bc4..f9fcf82b6 100644 --- a/backend/remote/backend_plan.go +++ b/backend/remote/backend_plan.go @@ -15,6 +15,7 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" + version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/tfdiags" ) @@ -70,14 +71,6 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio )) } - if op.Targets != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Resource targeting is currently not supported", - `The "remote" backend does not support resource targeting at this time.`, - )) - } - if b.hasExplicitVariableValues(op) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -106,6 +99,26 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio )) } + if len(op.Targets) != 0 { + // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, + // so if there's an error when parsing the RemoteAPIVersion, it's handled as + // equivalent to an API version < 2.3. + currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) + desiredAPIVersion, _ := version.NewVersion("2.3") + + if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource targeting is not supported", + fmt.Sprintf( + `The host %s does not support the -target option for `+ + `remote plans.`, + b.hostname, + ), + )) + } + } + // Return if there are any errors. if diags.HasErrors() { return nil, diags.Err() @@ -217,13 +230,29 @@ in order to capture the filesystem context the remote workspace expects: "Failed to upload configuration files", errors.New("operation timed out")) } + queueMessage := "Queued manually using Terraform" + if op.Targets != nil { + queueMessage = "Queued manually via Terraform using -target" + } + runOptions := tfe.RunCreateOptions{ IsDestroy: tfe.Bool(op.Destroy), - Message: tfe.String("Queued manually using Terraform"), + Message: tfe.String(queueMessage), ConfigurationVersion: cv, Workspace: w, } + if len(op.Targets) != 0 { + runOptions.TargetAddrs = make([]string, 0, len(op.Targets)) + for _, addr := range op.Targets { + // The API client wants the normal string representation of a + // target address, which will ultimately get inserted into a + // -target option when Terraform CLI is launched in the + // Cloud/Enterprise execution environment. + runOptions.TargetAddrs = append(runOptions.TargetAddrs, addr.String()) + } + } + r, err := b.client.Runs.Create(stopCtx, runOptions) if err != nil { return r, generalError("Failed to create run", err) diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go index 767b501d4..dd247e0c6 100644 --- a/backend/remote/backend_plan_test.go +++ b/backend/remote/backend_plan_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/backend" @@ -269,6 +270,29 @@ func TestRemote_planWithTarget(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() + // When the backend code creates a new run, we'll tweak it so that it + // has a cost estimation object with the "skipped_due_to_targeting" status, + // emulating how a real server is expected to behave in that case. + b.client.Runs.(*mockRuns).modifyNewRun = func(client *mockClient, options tfe.RunCreateOptions, run *tfe.Run) { + const fakeID = "fake" + // This is the cost estimate object embedded in the run itself which + // the backend will use to learn the ID to request from the cost + // estimates endpoint. It's pending to simulate what a freshly-created + // run is likely to look like. + run.CostEstimate = &tfe.CostEstimate{ + ID: fakeID, + Status: "pending", + } + // The backend will then use the main cost estimation API to retrieve + // the same ID indicated in the object above, where we'll then return + // the status "skipped_due_to_targeting" to trigger the special skip + // message in the backend output. + client.CostEstimates.estimations[fakeID] = &tfe.CostEstimate{ + ID: fakeID, + Status: "skipped_due_to_targeting", + } + } + op, configCleanup := testOperationPlan(t, "./testdata/plan") defer configCleanup() @@ -283,16 +307,34 @@ func TestRemote_planWithTarget(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { - t.Fatal("expected plan operation to fail") + if run.Result != backend.OperationSuccess { + t.Fatal("expected plan operation to succeed") } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") + if run.PlanEmpty { + t.Fatalf("expected plan to be non-empty") } - errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String() - if !strings.Contains(errOutput, "targeting is currently not supported") { - t.Fatalf("expected a targeting error, got: %v", errOutput) + // testBackendDefault above attached a "mock UI" to our backend, so we + // can retrieve its non-error output via the OutputWriter in-memory buffer. + gotOutput := b.CLI.(*cli.MockUi).OutputWriter.String() + if wantOutput := "Not available for this plan, because it was created with the -target option."; !strings.Contains(gotOutput, wantOutput) { + t.Errorf("missing message about skipped cost estimation\ngot:\n%s\nwant substring: %s", gotOutput, wantOutput) + } + + // We should find a run inside the mock client that has the same + // target address we requested above. + runsAPI := b.client.Runs.(*mockRuns) + if got, want := len(runsAPI.runs), 1; got != want { + t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) + } + for _, run := range runsAPI.runs { + if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { + t.Errorf("wrong TargetAddrs in the created run\n%s", diff) + } + + if !strings.Contains(run.Message, "using -target") { + t.Errorf("incorrect Message on the created run: %s", run.Message) + } } } diff --git a/backend/remote/testing.go b/backend/remote/testing.go index e506f28f9..3afaf97aa 100644 --- a/backend/remote/testing.go +++ b/backend/remote/testing.go @@ -207,6 +207,12 @@ func testServer(t *testing.T) *httptest.Server { }`, path.Base(r.URL.Path))) }) + // Respond to pings to get the API version header. + mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.3") + }) + // Respond to the initial query to read the hashicorp org entitlements. mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/vnd.api+json") diff --git a/go.mod b/go.mod index 0cb411c40..fb5d0c00d 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.5.2 github.com/hashicorp/go-rootcerts v1.0.0 github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect - github.com/hashicorp/go-tfe v0.3.27 + github.com/hashicorp/go-tfe v0.8.0 github.com/hashicorp/go-uuid v1.0.1 github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f diff --git a/go.sum b/go.sum index ab9ed0c07..45d9447ba 100644 --- a/go.sum +++ b/go.sum @@ -231,8 +231,8 @@ github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZ github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-tfe v0.3.27 h1:7XZ/ZoPyYoeuNXaWWW0mJOq016y0qb7I4Q0P/cagyu8= -github.com/hashicorp/go-tfe v0.3.27/go.mod h1:DVPSW2ogH+M9W1/i50ASgMht8cHP7NxxK0nrY9aFikQ= +github.com/hashicorp/go-tfe v0.8.0 h1:kz3x3tbIKRkEAzKg05P/qbFY88fkEU7TiSX3w8xUrmE= +github.com/hashicorp/go-tfe v0.8.0/go.mod h1:XAV72S4O1iP8BDaqiaPLmL2B4EE6almocnOn8E8stHc= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/vendor/github.com/hashicorp/go-tfe/README.md b/vendor/github.com/hashicorp/go-tfe/README.md index 3ebdb76b6..cba053e5a 100644 --- a/vendor/github.com/hashicorp/go-tfe/README.md +++ b/vendor/github.com/hashicorp/go-tfe/README.md @@ -26,19 +26,22 @@ Currently the following endpoints are supported: - [x] [OAuth Clients](https://www.terraform.io/docs/enterprise/api/oauth-clients.html) - [x] [OAuth Tokens](https://www.terraform.io/docs/enterprise/api/oauth-tokens.html) - [x] [Organizations](https://www.terraform.io/docs/enterprise/api/organizations.html) +- [x] [Organization Memberships](https://www.terraform.io/docs/cloud/api/organization-memberships.html) - [x] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.html) - [x] [Policies](https://www.terraform.io/docs/enterprise/api/policies.html) +- [x] [Policy Set Parameters](https://www.terraform.io/docs/enterprise/api/policy-set-params.html) - [x] [Policy Sets](https://www.terraform.io/docs/enterprise/api/policy-sets.html) - [x] [Policy Checks](https://www.terraform.io/docs/enterprise/api/policy-checks.html) - [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.html) - [x] [Runs](https://www.terraform.io/docs/enterprise/api/run.html) +- [x] [Run Triggers](https://www.terraform.io/docs/cloud/api/run-triggers.html) - [x] [SSH Keys](https://www.terraform.io/docs/enterprise/api/ssh-keys.html) - [x] [State Versions](https://www.terraform.io/docs/enterprise/api/state-versions.html) - [x] [Team Access](https://www.terraform.io/docs/enterprise/api/team-access.html) - [x] [Team Memberships](https://www.terraform.io/docs/enterprise/api/team-members.html) - [x] [Team Tokens](https://www.terraform.io/docs/enterprise/api/team-tokens.html) - [x] [Teams](https://www.terraform.io/docs/enterprise/api/teams.html) -- [x] [Variables](https://www.terraform.io/docs/enterprise/api/variables.html) +- [x] [Workspace Variables](https://www.terraform.io/docs/enterprise/api/workspace-variables.html) - [x] [Workspaces](https://www.terraform.io/docs/enterprise/api/workspaces.html) - [ ] [Admin](https://www.terraform.io/docs/enterprise/api/admin/index.html) @@ -145,7 +148,7 @@ and token. 1. `TFE_TOKEN` - A [user API token](https://www.terraform.io/docs/cloud/users-teams-organizations/users.html#api-tokens) for the Terraform Cloud or Terraform Enterprise instance being used for testing. ##### Optional: -1. `GITHUB_TOKEN` - [GitHub personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). Required for running OAuth client tests. +1. `GITHUB_TOKEN` - [GitHub personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). Required for running any tests that use VCS (OAuth clients, policy sets, etc). 1. `GITHUB_POLICY_SET_IDENTIFIER` - GitHub policy set repository identifier in the format `username/repository`. Required for running policy set tests. You can set your environment variables up however you prefer. The following are instructions for setting up environment variables using [envchain](https://github.com/sorah/envchain). @@ -233,4 +236,4 @@ Documentation updates and test fixes that only touch test files don't require a - Don't attach any binaries. The zip and tar.gz assets are automatically created and attached after you publish your release. - Click "Publish release" to save and publish your release. - \ No newline at end of file + diff --git a/vendor/github.com/hashicorp/go-tfe/go.mod b/vendor/github.com/hashicorp/go-tfe/go.mod index 31ad255a1..7871cb399 100644 --- a/vendor/github.com/hashicorp/go-tfe/go.mod +++ b/vendor/github.com/hashicorp/go-tfe/go.mod @@ -11,3 +11,5 @@ require ( github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 ) + +go 1.12 diff --git a/vendor/github.com/hashicorp/go-tfe/go.sum b/vendor/github.com/hashicorp/go-tfe/go.sum index 4c0ab53b1..74c1e9523 100644 --- a/vendor/github.com/hashicorp/go-tfe/go.sum +++ b/vendor/github.com/hashicorp/go-tfe/go.sum @@ -8,10 +8,6 @@ github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6K github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-retryablehttp v0.5.2 h1:AoISa4P4IsW0/m4T6St8Yw38gTl5GtBAgfkhYh1xAz4= github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-slug v0.4.0 h1:YSz3afoEZZJVVB46NITf0+opd2cHpaYJ1XSojOyP0x8= -github.com/hashicorp/go-slug v0.4.0/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= -github.com/hashicorp/go-slug v0.4.1-0.20191114211806-d9ee9eb3692a h1:EmBGX5Ja8JEKRHqTDG9+PYq0qL5qyOUmPZFQfH7VfXo= -github.com/hashicorp/go-slug v0.4.1-0.20191114211806-d9ee9eb3692a/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= github.com/hashicorp/go-slug v0.4.1 h1:/jAo8dNuLgSImoLXaX7Od7QB4TfYCVPam+OpAt5bZqc= github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= diff --git a/vendor/github.com/hashicorp/go-tfe/oauth_client.go b/vendor/github.com/hashicorp/go-tfe/oauth_client.go index 35174441b..3a3c3168e 100644 --- a/vendor/github.com/hashicorp/go-tfe/oauth_client.go +++ b/vendor/github.com/hashicorp/go-tfe/oauth_client.go @@ -120,6 +120,9 @@ type OAuthClientCreateOptions struct { // The token string you were given by your VCS provider. OAuthToken *string `jsonapi:"attr,oauth-token-string"` + // Private key associated with this vcs provider - only available for ado_server + PrivateKey *string `jsonapi:"attr,private-key"` + // The VCS provider being connected with. ServiceProvider *ServiceProviderType `jsonapi:"attr,service-provider"` } @@ -137,6 +140,9 @@ func (o OAuthClientCreateOptions) valid() error { if o.ServiceProvider == nil { return errors.New("service provider is required") } + if validString(o.PrivateKey) && *o.ServiceProvider != *ServiceProvider(ServiceProviderAzureDevOpsServer) { + return errors.New("Private Key can only be present with Azure DevOps Server service provider") + } return nil } diff --git a/vendor/github.com/hashicorp/go-tfe/organization_membership.go b/vendor/github.com/hashicorp/go-tfe/organization_membership.go new file mode 100644 index 000000000..2806f929e --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/organization_membership.go @@ -0,0 +1,178 @@ +package tfe + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ OrganizationMemberships = (*organizationMemberships)(nil) + +// OrganizationMemberships describes all the organization membership related methods that +// the Terraform Enterprise API supports. +// +// TFE API docs: +// https://www.terraform.io/docs/cloud/api/organization-memberships.html +type OrganizationMemberships interface { + // List all the organization memberships of the given organization. + List(ctx context.Context, organization string, options OrganizationMembershipListOptions) (*OrganizationMembershipList, error) + + // Create a new organization membership with the given options. + Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error) + + // Read an organization membership by ID + Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error) + + // Read an organization membership by ID with options + ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error) + + // Delete an organization membership by its ID. + Delete(ctx context.Context, organizationMembershipID string) error +} + +// organizationMemberships implements OrganizationMemberships. +type organizationMemberships struct { + client *Client +} + +// OrganizationMembershipStatus represents an organization membership status. +type OrganizationMembershipStatus string + +// List all available organization membership statuses. +const ( + OrganizationMembershipActive = "active" + OrganizationMembershipInvited = "invited" +) + +// OrganizationMembershipList represents a list of organization memberships. +type OrganizationMembershipList struct { + *Pagination + Items []*OrganizationMembership +} + +// OrganizationMembership represents a Terraform Enterprise organization membership. +type OrganizationMembership struct { + ID string `jsonapi:"primary,organization-memberships"` + Status OrganizationMembershipStatus `jsonapi:"attr,status"` + Email string `jsonapi:"attr,email"` + + // Relations + Organization *Organization `jsonapi:"relation,organization"` + User *User `jsonapi:"relation,user"` + Teams []*Team `jsonapi:"relation,teams"` +} + +// OrganizationMembershipListOptions represents the options for listing organization memberships. +type OrganizationMembershipListOptions struct { + ListOptions + + Include string `url:"include"` +} + +// List all the organization memberships of the given organization. +func (s *organizationMemberships) List(ctx context.Context, organization string, options OrganizationMembershipListOptions) (*OrganizationMembershipList, error) { + if !validStringID(&organization) { + return nil, errors.New("invalid value for organization") + } + + u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization)) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + ml := &OrganizationMembershipList{} + err = s.client.do(ctx, req, ml) + if err != nil { + return nil, err + } + + return ml, nil +} + +// OrganizationMembershipCreateOptions represents the options for creating an organization membership. +type OrganizationMembershipCreateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,organization-memberships"` + + // User's email address. + Email *string `jsonapi:"attr,email"` +} + +func (o OrganizationMembershipCreateOptions) valid() error { + if o.Email == nil { + return errors.New("email is required") + } + return nil +} + +// Create an organization membership with the given options. +func (s *organizationMemberships) Create(ctx context.Context, organization string, options OrganizationMembershipCreateOptions) (*OrganizationMembership, error) { + if !validStringID(&organization) { + return nil, errors.New("invalid value for organization") + } + if err := options.valid(); err != nil { + return nil, err + } + + options.ID = "" + + u := fmt.Sprintf("organizations/%s/organization-memberships", url.QueryEscape(organization)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + m := &OrganizationMembership{} + err = s.client.do(ctx, req, m) + if err != nil { + return nil, err + } + + return m, nil +} + +// Read an organization membership by its ID. +func (s *organizationMemberships) Read(ctx context.Context, organizationMembershipID string) (*OrganizationMembership, error) { + return s.ReadWithOptions(ctx, organizationMembershipID, OrganizationMembershipReadOptions{}) +} + +// OrganizationMembershipReadOptions represents the options for reading organization memberships. +type OrganizationMembershipReadOptions struct { + Include string `url:"include"` +} + +// Read an organization membership by ID with options +func (s *organizationMemberships) ReadWithOptions(ctx context.Context, organizationMembershipID string, options OrganizationMembershipReadOptions) (*OrganizationMembership, error) { + if !validStringID(&organizationMembershipID) { + return nil, errors.New("invalid value for membership") + } + + u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID)) + req, err := s.client.newRequest("GET", u, &options) + + mem := &OrganizationMembership{} + err = s.client.do(ctx, req, mem) + if err != nil { + return nil, err + } + + return mem, nil +} + +// Delete an organization membership by its ID. +func (s *organizationMemberships) Delete(ctx context.Context, organizationMembershipID string) error { + if !validStringID(&organizationMembershipID) { + return errors.New("invalid value for membership") + } + + u := fmt.Sprintf("organization-memberships/%s", url.QueryEscape(organizationMembershipID)) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} diff --git a/vendor/github.com/hashicorp/go-tfe/policy_set.go b/vendor/github.com/hashicorp/go-tfe/policy_set.go index b49e1bd2c..56a31e307 100644 --- a/vendor/github.com/hashicorp/go-tfe/policy_set.go +++ b/vendor/github.com/hashicorp/go-tfe/policy_set.go @@ -209,6 +209,19 @@ type PolicySetUpdateOptions struct { // Whether or not the policy set is global. Global *bool `jsonapi:"attr,global,omitempty"` + + // The sub-path within the attached VCS repository to ingress. All + // files and directories outside of this sub-path will be ignored. + // This option may only be specified when a VCS repo is present. + PoliciesPath *string `jsonapi:"attr,policies-path,omitempty"` + + // VCS repository information. When present, the policies and + // configuration will be sourced from the specified VCS repository + // instead of being defined within the policy set itself. Note that + // specifying this option may only be used on policy sets with no + // directly-attached policies (*PolicySet.Policies). Specifying this + // option when policies are already present will result in an error. + VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"` } func (o PolicySetUpdateOptions) valid() error { diff --git a/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go b/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go new file mode 100644 index 000000000..6e2587b90 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/policy_set_parameter.go @@ -0,0 +1,230 @@ +package tfe + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +// Compile-time proof of interface implementation. +var _ PolicySetParameters = (*policySetParameters)(nil) + +// PolicySetParameters describes all the parameter related methods that the Terraform +// Enterprise API supports. +// +// TFE API docs: https://www.terraform.io/docs/enterprise/api/policy-set-params.html +type PolicySetParameters interface { + // List all the parameters associated with the given policy-set. + List(ctx context.Context, policySetID string, options PolicySetParameterListOptions) (*PolicySetParameterList, error) + + // Create is used to create a new parameter. + Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error) + + // Read a parameter by its ID. + Read(ctx context.Context, policySetID string, parameterID string) (*PolicySetParameter, error) + + // Update values of an existing parameter. + Update(ctx context.Context, policySetID string, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error) + + // Delete a parameter by its ID. + Delete(ctx context.Context, policySetID string, parameterID string) error +} + +// policySetParameters implements Parameters. +type policySetParameters struct { + client *Client +} + +// PolicySetParameterList represents a list of parameters. +type PolicySetParameterList struct { + *Pagination + Items []*PolicySetParameter +} + +// PolicySetParameter represents a Policy Set parameter +type PolicySetParameter struct { + ID string `jsonapi:"primary,vars"` + Key string `jsonapi:"attr,key"` + Value string `jsonapi:"attr,value"` + Category CategoryType `jsonapi:"attr,category"` + Sensitive bool `jsonapi:"attr,sensitive"` + + // Relations + PolicySet *PolicySet `jsonapi:"relation,configurable"` +} + +// PolicySetParameterListOptions represents the options for listing parameters. +type PolicySetParameterListOptions struct { + ListOptions +} + +func (o PolicySetParameterListOptions) valid() error { + return nil +} + +// List all the parameters associated with the given policy-set. +func (s *policySetParameters) List(ctx context.Context, policySetID string, options PolicySetParameterListOptions) (*PolicySetParameterList, error) { + if !validStringID(&policySetID) { + return nil, errors.New("invalid value for policy set ID") + } + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("policy-sets/%s/parameters", policySetID) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + vl := &PolicySetParameterList{} + err = s.client.do(ctx, req, vl) + if err != nil { + return nil, err + } + + return vl, nil +} + +// PolicySetParameterCreateOptions represents the options for creating a new parameter. +type PolicySetParameterCreateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,vars"` + + // The name of the parameter. + Key *string `jsonapi:"attr,key"` + + // The value of the parameter. + Value *string `jsonapi:"attr,value,omitempty"` + + // The Category of the parameter, should always be "policy-set" + Category *CategoryType `jsonapi:"attr,category"` + + // Whether the value is sensitive. + Sensitive *bool `jsonapi:"attr,sensitive,omitempty"` +} + +func (o PolicySetParameterCreateOptions) valid() error { + if !validString(o.Key) { + return errors.New("key is required") + } + if o.Category == nil { + return errors.New("category is required") + } + if *o.Category != CategoryPolicySet { + return errors.New("category must be policy-set") + } + return nil +} + +// Create is used to create a new parameter. +func (s *policySetParameters) Create(ctx context.Context, policySetID string, options PolicySetParameterCreateOptions) (*PolicySetParameter, error) { + if !validStringID(&policySetID) { + return nil, errors.New("invalid value for policy set ID") + } + if err := options.valid(); err != nil { + return nil, err + } + + // Make sure we don't send a user provided ID. + options.ID = "" + + u := fmt.Sprintf("policy-sets/%s/parameters", url.QueryEscape(policySetID)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + p := &PolicySetParameter{} + err = s.client.do(ctx, req, p) + if err != nil { + return nil, err + } + + return p, nil +} + +// Read a parameter by its ID. +func (s *policySetParameters) Read(ctx context.Context, policySetID string, parameterID string) (*PolicySetParameter, error) { + if !validStringID(&policySetID) { + return nil, errors.New("invalid value for policy set ID") + } + if !validStringID(¶meterID) { + return nil, errors.New("invalid value for parameter ID") + } + + u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + p := &PolicySetParameter{} + err = s.client.do(ctx, req, p) + if err != nil { + return nil, err + } + + return p, err +} + +// PolicySetParameterUpdateOptions represents the options for updating a parameter. +type PolicySetParameterUpdateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,vars"` + + // The name of the parameter. + Key *string `jsonapi:"attr,key,omitempty"` + + // The value of the parameter. + Value *string `jsonapi:"attr,value,omitempty"` + + // Whether the value is sensitive. + Sensitive *bool `jsonapi:"attr,sensitive,omitempty"` +} + +// Update values of an existing parameter. +func (s *policySetParameters) Update(ctx context.Context, policySetID string, parameterID string, options PolicySetParameterUpdateOptions) (*PolicySetParameter, error) { + if !validStringID(&policySetID) { + return nil, errors.New("invalid value for policy set ID") + } + if !validStringID(¶meterID) { + return nil, errors.New("invalid value for parameter ID") + } + + // Make sure we don't send a user provided ID. + options.ID = parameterID + + u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) + req, err := s.client.newRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + p := &PolicySetParameter{} + err = s.client.do(ctx, req, p) + if err != nil { + return nil, err + } + + return p, nil +} + +// Delete a parameter by its ID. +func (s *policySetParameters) Delete(ctx context.Context, policySetID string, parameterID string) error { + if !validStringID(&policySetID) { + return errors.New("invalid value for policy set ID") + } + if !validStringID(¶meterID) { + return errors.New("invalid value for parameter ID") + } + + u := fmt.Sprintf("policy-sets/%s/parameters/%s", url.QueryEscape(policySetID), url.QueryEscape(parameterID)) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} diff --git a/vendor/github.com/hashicorp/go-tfe/run.go b/vendor/github.com/hashicorp/go-tfe/run.go index 4517bf262..352ea64d2 100644 --- a/vendor/github.com/hashicorp/go-tfe/run.go +++ b/vendor/github.com/hashicorp/go-tfe/run.go @@ -98,6 +98,7 @@ type Run struct { Source RunSource `jsonapi:"attr,source"` Status RunStatus `jsonapi:"attr,status"` StatusTimestamps *RunStatusTimestamps `jsonapi:"attr,status-timestamps"` + TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"` // Relations Apply *Apply `jsonapi:"relation,apply"` @@ -184,6 +185,19 @@ type RunCreateOptions struct { // Specifies the workspace where the run will be executed. Workspace *Workspace `jsonapi:"relation,workspace"` + + // If non-empty, requests that Terraform should create a plan including + // actions only for the given objects (specified using resource address + // syntax) and the objects they depend on. + // + // This capability is provided for exceptional circumstances only, such as + // recovering from mistakes or working around existing Terraform + // limitations. Terraform will generally mention the -target command line + // option in its error messages describing situations where setting this + // argument may be appropriate. This argument should not be used as part + // of routine workflow and Terraform will emit warnings reminding about + // this whenever this property is set. + TargetAddrs []string `jsonapi:"attr,target-addrs,omitempty"` } func (o RunCreateOptions) valid() error { diff --git a/vendor/github.com/hashicorp/go-tfe/run_trigger.go b/vendor/github.com/hashicorp/go-tfe/run_trigger.go new file mode 100644 index 000000000..a4c6f4f65 --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/run_trigger.go @@ -0,0 +1,177 @@ +package tfe + +import ( + "context" + "errors" + "fmt" + "net/url" + "time" +) + +// Compile-time proof of interface implementation. +var _ RunTriggers = (*runTriggers)(nil) + +// RunTriggers describes all the Run Trigger +// related methods that the Terraform Cloud API supports. +// +// TFE API docs: +// https://www.terraform.io/docs/cloud/api/run-triggers.html +type RunTriggers interface { + // List all the run triggers within a workspace. + List(ctx context.Context, workspaceID string, options RunTriggerListOptions) (*RunTriggerList, error) + + // Create a new run trigger with the given options. + Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error) + + // Read a run trigger by its ID. + Read(ctx context.Context, RunTriggerID string) (*RunTrigger, error) + + // Delete a run trigger by its ID. + Delete(ctx context.Context, RunTriggerID string) error +} + +// runTriggers implements RunTriggers. +type runTriggers struct { + client *Client +} + +// RunTriggerList represents a list of Run Triggers +type RunTriggerList struct { + *Pagination + Items []*RunTrigger +} + +// 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"` +} + +// RunTriggerListOptions represents the options for listing +// run triggers. +type RunTriggerListOptions struct { + ListOptions + RunTriggerType *string `url:"filter[run-trigger][type]"` +} + +func (o RunTriggerListOptions) valid() error { + if !validString(o.RunTriggerType) { + return errors.New("run-trigger type is required") + } + if *o.RunTriggerType != "inbound" && *o.RunTriggerType != "outbound" { + return errors.New("invalid value for run-trigger type") + } + return nil +} + +// List all the run triggers associated with a workspace. +func (s *runTriggers) List(ctx context.Context, workspaceID string, options RunTriggerListOptions) (*RunTriggerList, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + rtl := &RunTriggerList{} + err = s.client.do(ctx, req, rtl) + if err != nil { + return nil, err + } + + return rtl, nil +} + +// RunTriggerCreateOptions represents the options for +// creating a new run trigger. +type RunTriggerCreateOptions struct { + // For internal use only! + ID string `jsonapi:"primary,run-triggers"` + + // The source workspace + Sourceable *Workspace `jsonapi:"relation,sourceable"` +} + +func (o RunTriggerCreateOptions) valid() error { + if o.Sourceable == nil { + return errors.New("sourceable is required") + } + return nil +} + +// Creates a run trigger with the given options. +func (s *runTriggers) Create(ctx context.Context, workspaceID string, options RunTriggerCreateOptions) (*RunTrigger, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") + } + if err := options.valid(); err != nil { + return nil, err + } + + // Make sure we don't send a user provided ID. + options.ID = "" + + u := fmt.Sprintf("workspaces/%s/run-triggers", url.QueryEscape(workspaceID)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + rt := &RunTrigger{} + err = s.client.do(ctx, req, rt) + if err != nil { + return nil, err + } + + return rt, nil +} + +// Read a run trigger by its ID. +func (s *runTriggers) Read(ctx context.Context, runTriggerID string) (*RunTrigger, error) { + if !validStringID(&runTriggerID) { + return nil, errors.New("invalid value for run trigger ID") + } + + u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + rt := &RunTrigger{} + err = s.client.do(ctx, req, rt) + if err != nil { + return nil, err + } + + return rt, nil +} + +// Delete a run trigger by its ID. +func (s *runTriggers) Delete(ctx context.Context, runTriggerID string) error { + if !validStringID(&runTriggerID) { + return errors.New("invalid value for run trigger ID") + } + + u := fmt.Sprintf("run-triggers/%s", url.QueryEscape(runTriggerID)) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} diff --git a/vendor/github.com/hashicorp/go-tfe/team.go b/vendor/github.com/hashicorp/go-tfe/team.go index 7bd93a4f9..3828c5c76 100644 --- a/vendor/github.com/hashicorp/go-tfe/team.go +++ b/vendor/github.com/hashicorp/go-tfe/team.go @@ -47,11 +47,13 @@ type Team struct { ID string `jsonapi:"primary,teams"` Name string `jsonapi:"attr,name"` OrganizationAccess *OrganizationAccess `jsonapi:"attr,organization-access"` + Visibility string `jsonapi:"attr,visibility"` Permissions *TeamPermissions `jsonapi:"attr,permissions"` UserCount int `jsonapi:"attr,users-count"` // Relations - Users []*User `jsonapi:"relation,users"` + Users []*User `jsonapi:"relation,users"` + OrganizationMemberships []*OrganizationMembership `jsonapi:"relation,organization-memberships"` } // OrganizationAccess represents the team's permissions on its organization @@ -103,6 +105,9 @@ type TeamCreateOptions struct { // The team's organization access OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"` + + // The team's visibility ("secret", "organization") + Visibility *string `jsonapi:"attr,visibility,omitempty"` } // OrganizationAccessOptions represents the organization access options of a team. @@ -177,6 +182,9 @@ type TeamUpdateOptions struct { // The team's organization access OrganizationAccess *OrganizationAccessOptions `jsonapi:"attr,organization-access,omitempty"` + + // The team's visibility ("secret", "organization") + Visibility *string `jsonapi:"attr,visibility,omitempty"` } // Update a team by its ID. diff --git a/vendor/github.com/hashicorp/go-tfe/team_member.go b/vendor/github.com/hashicorp/go-tfe/team_member.go index 8ab97fdac..450e57472 100644 --- a/vendor/github.com/hashicorp/go-tfe/team_member.go +++ b/vendor/github.com/hashicorp/go-tfe/team_member.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "net/url" + + retryablehttp "github.com/hashicorp/go-retryablehttp" ) // Compile-time proof of interface implementation. @@ -16,9 +18,16 @@ var _ TeamMembers = (*teamMembers)(nil) // TFE API docs: // https://www.terraform.io/docs/enterprise/api/team-members.html type TeamMembers interface { - // List all members of a team. + // List returns all Users of a team calling ListUsers + // See ListOrganizationMemberships for fetching memberships List(ctx context.Context, teamID string) ([]*User, error) + // ListUsers returns the Users of this team. + ListUsers(ctx context.Context, teamID string) ([]*User, error) + + // ListOrganizationMemberships returns the OrganizationMemberships of this team. + ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error) + // Add multiple users to a team. Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error @@ -31,12 +40,22 @@ type teamMembers struct { client *Client } -type teamMember struct { +type teamMemberUser struct { Username string `jsonapi:"primary,users"` } -// List all members of a team. +type teamMemberOrgMembership struct { + ID string `jsonapi:"primary,organization-memberships"` +} + +// List returns all Users of a team calling ListUsers +// See ListOrganizationMemberships for fetching memberships func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) { + return s.ListUsers(ctx, teamID) +} + +// ListUsers returns the Users of this team. +func (s *teamMembers) ListUsers(ctx context.Context, teamID string) ([]*User, error) { if !validStringID(&teamID) { return nil, errors.New("invalid value for team ID") } @@ -62,21 +81,65 @@ func (s *teamMembers) List(ctx context.Context, teamID string) ([]*User, error) return t.Users, nil } -// TeamMemberAddOptions represents the options for adding team members. +// ListOrganizationMemberships returns the OrganizationMemberships of this team. +func (s *teamMembers) ListOrganizationMemberships(ctx context.Context, teamID string) ([]*OrganizationMembership, error) { + if !validStringID(&teamID) { + return nil, errors.New("invalid value for team ID") + } + + options := struct { + Include string `url:"include"` + }{ + Include: "organization-memberships", + } + + u := fmt.Sprintf("teams/%s", url.QueryEscape(teamID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + t := &Team{} + err = s.client.do(ctx, req, t) + if err != nil { + return nil, err + } + + return t.OrganizationMemberships, nil +} + +// TeamMemberAddOptions represents the options for +// adding or removing team members. type TeamMemberAddOptions struct { - Usernames []string + Usernames []string + OrganizationMembershipIDs []string } func (o *TeamMemberAddOptions) valid() error { - if o.Usernames == nil { - return errors.New("usernames is required") + if o.Usernames == nil && o.OrganizationMembershipIDs == nil { + return errors.New("usernames or organization membership ids are required") } - if len(o.Usernames) == 0 { + if o.Usernames != nil && o.OrganizationMembershipIDs != nil { + return errors.New("only one of usernames or organization membership ids can be provided") + } + if o.Usernames != nil && len(o.Usernames) == 0 { return errors.New("invalid value for usernames") } + if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 { + return errors.New("invalid value for organization membership ids") + } return nil } +// kind returns "users" or "organization-memberships" +// depending on which is defined +func (o *TeamMemberAddOptions) kind() string { + if o.Usernames != nil && len(o.Usernames) != 0 { + return "users" + } + return "organization-memberships" +} + // Add multiple users to a team. func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMemberAddOptions) error { if !validStringID(&teamID) { @@ -86,35 +149,68 @@ func (s *teamMembers) Add(ctx context.Context, teamID string, options TeamMember return err } - var tms []*teamMember - for _, name := range options.Usernames { - tms = append(tms, &teamMember{Username: name}) - } + usersOrMemberships := options.kind() + URL := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships) - u := fmt.Sprintf("teams/%s/relationships/users", url.QueryEscape(teamID)) - req, err := s.client.newRequest("POST", u, tms) - if err != nil { - return err + var req *retryablehttp.Request + + if usersOrMemberships == "users" { + var err error + var members []*teamMemberUser + for _, name := range options.Usernames { + members = append(members, &teamMemberUser{Username: name}) + } + req, err = s.client.newRequest("POST", URL, members) + if err != nil { + return err + } + } else { + var err error + var members []*teamMemberOrgMembership + for _, ID := range options.OrganizationMembershipIDs { + members = append(members, &teamMemberOrgMembership{ID: ID}) + } + req, err = s.client.newRequest("POST", URL, members) + if err != nil { + return err + } } return s.client.do(ctx, req, nil) } -// TeamMemberRemoveOptions represents the options for deleting team members. +// TeamMemberRemoveOptions represents the options for +// adding or removing team members. type TeamMemberRemoveOptions struct { - Usernames []string + Usernames []string + OrganizationMembershipIDs []string } func (o *TeamMemberRemoveOptions) valid() error { - if o.Usernames == nil { - return errors.New("usernames is required") + if o.Usernames == nil && o.OrganizationMembershipIDs == nil { + return errors.New("usernames or organization membership ids are required") } - if len(o.Usernames) == 0 { + if o.Usernames != nil && o.OrganizationMembershipIDs != nil { + return errors.New("only one of usernames or organization membership ids can be provided") + } + if o.Usernames != nil && len(o.Usernames) == 0 { return errors.New("invalid value for usernames") } + if o.OrganizationMembershipIDs != nil && len(o.OrganizationMembershipIDs) == 0 { + return errors.New("invalid value for organization membership ids") + } return nil } +// kind returns "users" or "organization-memberships" +// depending on which is defined +func (o *TeamMemberRemoveOptions) kind() string { + if o.Usernames != nil && len(o.Usernames) != 0 { + return "users" + } + return "organization-memberships" +} + // Remove multiple users from a team. func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMemberRemoveOptions) error { if !validStringID(&teamID) { @@ -124,15 +220,31 @@ func (s *teamMembers) Remove(ctx context.Context, teamID string, options TeamMem return err } - var tms []*teamMember - for _, name := range options.Usernames { - tms = append(tms, &teamMember{Username: name}) - } + usersOrMemberships := options.kind() + URL := fmt.Sprintf("teams/%s/relationships/%s", url.QueryEscape(teamID), usersOrMemberships) - u := fmt.Sprintf("teams/%s/relationships/users", url.QueryEscape(teamID)) - req, err := s.client.newRequest("DELETE", u, tms) - if err != nil { - return err + var req *retryablehttp.Request + + if usersOrMemberships == "users" { + var err error + var members []*teamMemberUser + for _, name := range options.Usernames { + members = append(members, &teamMemberUser{Username: name}) + } + req, err = s.client.newRequest("DELETE", URL, members) + if err != nil { + return err + } + } else { + var err error + var members []*teamMemberOrgMembership + for _, ID := range options.OrganizationMembershipIDs { + members = append(members, &teamMemberOrgMembership{ID: ID}) + } + req, err = s.client.newRequest("DELETE", URL, members) + if err != nil { + return err + } } return s.client.do(ctx, req, nil) diff --git a/vendor/github.com/hashicorp/go-tfe/testing.go b/vendor/github.com/hashicorp/go-tfe/testing.go new file mode 100644 index 000000000..79f9c218a --- /dev/null +++ b/vendor/github.com/hashicorp/go-tfe/testing.go @@ -0,0 +1,36 @@ +package tfe + +import ( + "context" + "testing" +) + +// TestAccountDetails represents the basic account information +// of a TFE/TFC user. +// +// See FetchTestAccountDetails for more information. +type TestAccountDetails struct { + ID string `json:"id" jsonapi:"primary,users"` + Username string `jsonapi:"attr,username"` + Email string `jsonapi:"attr,email"` +} + +// FetchTestAccountDetails returns TestAccountDetails +// of the user running the tests. +// +// Use this helper to fetch the username and email +// address associated with the token used to run the tests. +func FetchTestAccountDetails(t *testing.T, client *Client) *TestAccountDetails { + tad := &TestAccountDetails{} + req, err := client.newRequest("GET", "account/details", nil) + if err != nil { + t.Fatalf("could not create account details request: %v", err) + } + + ctx := context.Background() + err = client.do(ctx, req, tad) + if err != nil { + t.Fatalf("could not fetch test user details: %v", err) + } + return tad +} diff --git a/vendor/github.com/hashicorp/go-tfe/tfe.go b/vendor/github.com/hashicorp/go-tfe/tfe.go index da3435a65..5883d9d87 100644 --- a/vendor/github.com/hashicorp/go-tfe/tfe.go +++ b/vendor/github.com/hashicorp/go-tfe/tfe.go @@ -24,15 +24,16 @@ import ( ) const ( - userAgent = "go-tfe" - headerRateLimit = "X-RateLimit-Limit" - headerRateReset = "X-RateLimit-Reset" + userAgent = "go-tfe" + headerRateLimit = "X-RateLimit-Limit" + headerRateReset = "X-RateLimit-Reset" + headerAPIVersion = "TFP-API-Version" // DefaultAddress of Terraform Enterprise. DefaultAddress = "https://app.terraform.io" // DefaultBasePath on which the API is served. DefaultBasePath = "/api/v2/" - // No-op API endpoint used to configure the rate limiter + // PingEndpoint is a no-op API endpoint used to configure the rate limiter PingEndpoint = "ping" ) @@ -105,6 +106,7 @@ type Client struct { limiter *rate.Limiter retryLogHook RetryLogHook retryServerErrors bool + remoteAPIVersion string Applies Applies ConfigurationVersions ConfigurationVersions @@ -113,13 +115,16 @@ type Client struct { OAuthClients OAuthClients OAuthTokens OAuthTokens Organizations Organizations + OrganizationMemberships OrganizationMemberships OrganizationTokens OrganizationTokens Plans Plans PlanExports PlanExports Policies Policies PolicyChecks PolicyChecks + PolicySetParameters PolicySetParameters PolicySets PolicySets Runs Runs + RunTriggers RunTriggers SSHKeys SSHKeys StateVersions StateVersions Teams Teams @@ -191,11 +196,18 @@ func NewClient(cfg *Config) (*Client, error) { RetryMax: 30, } - // Configure the rate limiter. - if err := client.configureLimiter(); err != nil { + meta, err := client.getRawAPIMetadata() + if err != nil { return nil, err } + // Configure the rate limiter. + client.configureLimiter(meta.RateLimit) + + // Save the API version so we can return it from the RemoteAPIVersion + // method later. + client.remoteAPIVersion = meta.APIVersion + // Create the services. client.Applies = &applies{client: client} client.ConfigurationVersions = &configurationVersions{client: client} @@ -204,13 +216,16 @@ func NewClient(cfg *Config) (*Client, error) { client.OAuthClients = &oAuthClients{client: client} client.OAuthTokens = &oAuthTokens{client: client} client.Organizations = &organizations{client: client} + client.OrganizationMemberships = &organizationMemberships{client: client} client.OrganizationTokens = &organizationTokens{client: client} client.Plans = &plans{client: client} client.PlanExports = &planExports{client: client} client.Policies = &policies{client: client} client.PolicyChecks = &policyChecks{client: client} + client.PolicySetParameters = &policySetParameters{client: client} client.PolicySets = &policySets{client: client} client.Runs = &runs{client: client} + client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} client.StateVersions = &stateVersions{client: client} client.Teams = &teams{client: client} @@ -224,6 +239,26 @@ func NewClient(cfg *Config) (*Client, error) { return client, nil } +// RemoteAPIVersion returns the server's declared API version string. +// +// A Terraform Cloud or Enterprise API server returns its API version in an +// HTTP header field in all responses. The NewClient function saves the +// version number returned in its initial setup request and RemoteAPIVersion +// returns that cached value. +// +// The API protocol calls for this string to be a dotted-decimal version number +// like 2.3.0, where the first number indicates the API major version while the +// second indicates a minor version which may have introduced some +// backward-compatible additional features compared to its predecessor. +// +// Explicit API versioning was added to the Terraform Cloud and Enterprise +// APIs as a later addition, so older servers will not return version +// information. In that case, this function returns an empty string as the +// version. +func (c *Client) RemoteAPIVersion() string { + return c.remoteAPIVersion +} + // RetryServerErrors configures the retry HTTP check to also retry // unexpected errors or requests that failed with a server error. func (c *Client) RetryServerErrors(retry bool) { @@ -292,16 +327,29 @@ func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Respons return min + jitter } -// configureLimiter configures the rate limiter. -func (c *Client) configureLimiter() error { +type rawAPIMetadata struct { + // APIVersion is the raw API version string reported by the server in the + // TFP-API-Version response header, or an empty string if that header + // field was not included in the response. + APIVersion string + + // RateLimit is the raw API version string reported by the server in the + // X-RateLimit-Limit response header, or an empty string if that header + // field was not included in the response. + RateLimit string +} + +func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) { + var meta rawAPIMetadata + // Create a new request. u, err := c.baseURL.Parse(PingEndpoint) if err != nil { - return err + return meta, err } req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - return err + return meta, err } // Attach the default headers. @@ -314,15 +362,24 @@ func (c *Client) configureLimiter() error { // Make a single request to retrieve the rate limit headers. resp, err := c.http.HTTPClient.Do(req) if err != nil { - return err + return meta, err } resp.Body.Close() + meta.APIVersion = resp.Header.Get(headerAPIVersion) + meta.RateLimit = resp.Header.Get(headerRateLimit) + + return meta, nil +} + +// configureLimiter configures the rate limiter. +func (c *Client) configureLimiter(rawLimit string) { + // Set default values for when rate limiting is disabled. limit := rate.Inf burst := 0 - if v := resp.Header.Get(headerRateLimit); v != "" { + if v := rawLimit; v != "" { if rateLimit, _ := strconv.ParseFloat(v, 64); rateLimit > 0 { // Configure the limit and burst using a split of 2/3 for the limit and // 1/3 for the burst. This enables clients to burst 1/3 of the allowed @@ -336,8 +393,6 @@ func (c *Client) configureLimiter() error { // Create a new limiter using the calculated values. c.limiter = rate.NewLimiter(limit, burst) - - return nil } // newRequest creates an API request. A relative URL path can be provided in diff --git a/vendor/github.com/hashicorp/go-tfe/variable.go b/vendor/github.com/hashicorp/go-tfe/variable.go index eb3dbc6e5..6ac8b5969 100644 --- a/vendor/github.com/hashicorp/go-tfe/variable.go +++ b/vendor/github.com/hashicorp/go-tfe/variable.go @@ -16,19 +16,19 @@ var _ Variables = (*variables)(nil) // TFE API docs: https://www.terraform.io/docs/enterprise/api/variables.html type Variables interface { // List all the variables associated with the given workspace. - List(ctx context.Context, options VariableListOptions) (*VariableList, error) + List(ctx context.Context, workspaceID string, options VariableListOptions) (*VariableList, error) // Create is used to create a new variable. - Create(ctx context.Context, options VariableCreateOptions) (*Variable, error) + Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error) // Read a variable by its ID. - Read(ctx context.Context, variableID string) (*Variable, error) + Read(ctx context.Context, workspaceID string, variableID string) (*Variable, error) // Update values of an existing variable. - Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) + Update(ctx context.Context, workspaceID string, variableID string, options VariableUpdateOptions) (*Variable, error) // Delete a variable by its ID. - Delete(ctx context.Context, variableID string) error + Delete(ctx context.Context, workspaceID string, variableID string) error } // variables implements Variables. @@ -42,6 +42,7 @@ type CategoryType string //List all available categories. const ( CategoryEnv CategoryType = "env" + CategoryPolicySet CategoryType = "policy-set" CategoryTerraform CategoryType = "terraform" ) @@ -56,38 +57,28 @@ type Variable struct { ID string `jsonapi:"primary,vars"` Key string `jsonapi:"attr,key"` Value string `jsonapi:"attr,value"` + Description string `jsonapi:"attr,description"` Category CategoryType `jsonapi:"attr,category"` HCL bool `jsonapi:"attr,hcl"` Sensitive bool `jsonapi:"attr,sensitive"` // Relations - Workspace *Workspace `jsonapi:"relation,workspace"` + Workspace *Workspace `jsonapi:"relation,configurable"` } // VariableListOptions represents the options for listing variables. type VariableListOptions struct { ListOptions - Organization *string `url:"filter[organization][name]"` - Workspace *string `url:"filter[workspace][name]"` -} - -func (o VariableListOptions) valid() error { - if !validString(o.Organization) { - return errors.New("organization is required") - } - if !validString(o.Workspace) { - return errors.New("workspace is required") - } - return nil } // List all the variables associated with the given workspace. -func (s *variables) List(ctx context.Context, options VariableListOptions) (*VariableList, error) { - if err := options.valid(); err != nil { - return nil, err +func (s *variables) List(ctx context.Context, workspaceID string, options VariableListOptions) (*VariableList, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") } - req, err := s.client.newRequest("GET", "vars", &options) + u := fmt.Sprintf("workspaces/%s/vars", workspaceID) + req, err := s.client.newRequest("GET", u, &options) if err != nil { return nil, err } @@ -112,6 +103,9 @@ type VariableCreateOptions struct { // The value of the variable. Value *string `jsonapi:"attr,value,omitempty"` + // The description of the variable. + Description *string `jsonapi:"attr,description,omitempty"` + // Whether this is a Terraform or environment variable. Category *CategoryType `jsonapi:"attr,category"` @@ -120,9 +114,6 @@ type VariableCreateOptions struct { // Whether the value is sensitive. Sensitive *bool `jsonapi:"attr,sensitive,omitempty"` - - // The workspace that owns the variable. - Workspace *Workspace `jsonapi:"relation,workspace"` } func (o VariableCreateOptions) valid() error { @@ -132,14 +123,14 @@ func (o VariableCreateOptions) valid() error { if o.Category == nil { return errors.New("category is required") } - if o.Workspace == nil { - return errors.New("workspace is required") - } return nil } // Create is used to create a new variable. -func (s *variables) Create(ctx context.Context, options VariableCreateOptions) (*Variable, error) { +func (s *variables) Create(ctx context.Context, workspaceID string, options VariableCreateOptions) (*Variable, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") + } if err := options.valid(); err != nil { return nil, err } @@ -147,7 +138,8 @@ func (s *variables) Create(ctx context.Context, options VariableCreateOptions) ( // Make sure we don't send a user provided ID. options.ID = "" - req, err := s.client.newRequest("POST", "vars", &options) + u := fmt.Sprintf("workspaces/%s/vars", url.QueryEscape(workspaceID)) + req, err := s.client.newRequest("POST", u, &options) if err != nil { return nil, err } @@ -162,12 +154,15 @@ func (s *variables) Create(ctx context.Context, options VariableCreateOptions) ( } // Read a variable by its ID. -func (s *variables) Read(ctx context.Context, variableID string) (*Variable, error) { +func (s *variables) Read(ctx context.Context, workspaceID string, variableID string) (*Variable, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") + } if !validStringID(&variableID) { return nil, errors.New("invalid value for variable ID") } - u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID)) + u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) req, err := s.client.newRequest("GET", u, nil) if err != nil { return nil, err @@ -193,6 +188,9 @@ type VariableUpdateOptions struct { // The value of the variable. Value *string `jsonapi:"attr,value,omitempty"` + // The description of the variable. + Description *string `jsonapi:"attr,description,omitempty"` + // Whether to evaluate the value of the variable as a string of HCL code. HCL *bool `jsonapi:"attr,hcl,omitempty"` @@ -201,7 +199,10 @@ type VariableUpdateOptions struct { } // Update values of an existing variable. -func (s *variables) Update(ctx context.Context, variableID string, options VariableUpdateOptions) (*Variable, error) { +func (s *variables) Update(ctx context.Context, workspaceID string, variableID string, options VariableUpdateOptions) (*Variable, error) { + if !validStringID(&workspaceID) { + return nil, errors.New("invalid value for workspace ID") + } if !validStringID(&variableID) { return nil, errors.New("invalid value for variable ID") } @@ -209,7 +210,7 @@ func (s *variables) Update(ctx context.Context, variableID string, options Varia // Make sure we don't send a user provided ID. options.ID = variableID - u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID)) + u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) req, err := s.client.newRequest("PATCH", u, &options) if err != nil { return nil, err @@ -225,12 +226,15 @@ func (s *variables) Update(ctx context.Context, variableID string, options Varia } // Delete a variable by its ID. -func (s *variables) Delete(ctx context.Context, variableID string) error { +func (s *variables) Delete(ctx context.Context, workspaceID string, variableID string) error { + if !validStringID(&workspaceID) { + return errors.New("invalid value for workspace ID") + } if !validStringID(&variableID) { return errors.New("invalid value for variable ID") } - u := fmt.Sprintf("vars/%s", url.QueryEscape(variableID)) + u := fmt.Sprintf("workspaces/%s/vars/%s", url.QueryEscape(workspaceID), url.QueryEscape(variableID)) req, err := s.client.newRequest("DELETE", u, nil) if err != nil { return err diff --git a/vendor/modules.txt b/vendor/modules.txt index 74e0ac417..776abd04d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -348,7 +348,7 @@ github.com/hashicorp/go-safetemp github.com/hashicorp/go-slug # github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 ## explicit -# github.com/hashicorp/go-tfe v0.3.27 +# github.com/hashicorp/go-tfe v0.8.0 ## explicit github.com/hashicorp/go-tfe # github.com/hashicorp/go-uuid v1.0.1