backend/remote: Support -target on plan and apply
This commit is contained in:
commit
c1f69fba03
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
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
|
||||
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,16 +220,32 @@ 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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -27,12 +27,13 @@ const (
|
|||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue