Merge pull request #22761 from hashicorp/pault/tfce-ga

Remote Backend: Support latest cost-estimate API
This commit is contained in:
Paul Thrasher 2019-09-30 14:34:51 -07:00 committed by GitHub
commit 6f313abc9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 751 additions and 152 deletions

View File

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"io" "io"
"math" "math"
"strconv"
"strings"
"time" "time"
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
@ -227,6 +229,87 @@ func (b *Remote) parseVariableValues(op *backend.Operation) (terraform.InputValu
return result, diags return result, diags
} }
func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
if r.CostEstimate == nil {
return nil
}
if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------\n")
}
msgPrefix := "Cost estimation"
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
}
started := time.Now()
updated := started
for i := 0; ; i++ {
select {
case <-stopCtx.Done():
return stopCtx.Err()
case <-cancelCtx.Done():
return cancelCtx.Err()
case <-time.After(1 * time.Second):
}
// Retrieve the cost estimate to get its current status.
ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID)
if err != nil {
return generalError("Failed to retrieve cost estimate", err)
}
switch ce.Status {
case tfe.CostEstimateFinished:
delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64)
if err != nil {
return generalError("Unexpected error", err)
}
sign := "+"
if delta < 0 {
sign = "-"
}
deltaRepr := strings.Replace(ce.DeltaMonthlyCost, "-", "", 1)
if b.CLI != nil {
b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount)))
b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr)))
if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply {
b.CLI.Output("\n------------------------------------------------------------------------")
}
}
return nil
case tfe.CostEstimatePending, tfe.CostEstimateQueued:
// Check if 30 seconds have passed since the last update.
current := time.Now()
if b.CLI != nil && (i == 0 || current.Sub(updated).Seconds() > 30) {
updated = current
elapsed := ""
// Calculate and set the elapsed time.
if i > 0 {
elapsed = fmt.Sprintf(
" (%s elapsed)", current.Sub(started).Truncate(30*time.Second))
}
b.CLI.Output(b.Colorize().Color("Waiting for cost estimate to complete..." + elapsed + "\n"))
}
continue
case tfe.CostEstimateErrored:
return fmt.Errorf(msgPrefix + " errored.")
case tfe.CostEstimateCanceled:
return fmt.Errorf(msgPrefix + " canceled.")
default:
return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status)
}
}
return nil
}
func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
if b.CLI != nil { if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------\n") b.CLI.Output("\n------------------------------------------------------------------------\n")

View File

@ -21,6 +21,7 @@ import (
type mockClient struct { type mockClient struct {
Applies *mockApplies Applies *mockApplies
ConfigurationVersions *mockConfigurationVersions ConfigurationVersions *mockConfigurationVersions
CostEstimates *mockCostEstimates
Organizations *mockOrganizations Organizations *mockOrganizations
Plans *mockPlans Plans *mockPlans
PolicyChecks *mockPolicyChecks PolicyChecks *mockPolicyChecks
@ -33,6 +34,7 @@ func newMockClient() *mockClient {
c := &mockClient{} c := &mockClient{}
c.Applies = newMockApplies(c) c.Applies = newMockApplies(c)
c.ConfigurationVersions = newMockConfigurationVersions(c) c.ConfigurationVersions = newMockConfigurationVersions(c)
c.CostEstimates = newMockCostEstimates(c)
c.Organizations = newMockOrganizations(c) c.Organizations = newMockOrganizations(c)
c.Plans = newMockPlans(c) c.Plans = newMockPlans(c)
c.PolicyChecks = newMockPolicyChecks(c) c.PolicyChecks = newMockPolicyChecks(c)
@ -212,6 +214,88 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
return nil return nil
} }
type mockCostEstimates struct {
client *mockClient
estimations map[string]*tfe.CostEstimate
logs map[string]string
}
func newMockCostEstimates(client *mockClient) *mockCostEstimates {
return &mockCostEstimates{
client: client,
estimations: make(map[string]*tfe.CostEstimate),
logs: make(map[string]string),
}
}
// create is a helper function to create a mock cost estimation that uses the
// configured working directory to find the logfile.
func (m *mockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) {
id := generateID("ce-")
ce := &tfe.CostEstimate{
ID: id,
MatchedResourcesCount: 1,
ResourcesCount: 1,
DeltaMonthlyCost: "0.00",
ProposedMonthlyCost: "0.00",
Status: tfe.CostEstimateFinished,
}
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile := filepath.Join(
m.client.ConfigurationVersions.uploadPaths[cvID],
w.WorkingDirectory,
"cost-estimate.log",
)
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return nil, nil
}
m.logs[ce.ID] = logfile
m.estimations[ce.ID] = ce
return ce, nil
}
func (m *mockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) {
ce, ok := m.estimations[costEstimateID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return ce, nil
}
func (m *mockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
ce, ok := m.estimations[costEstimateID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
logfile, ok := m.logs[ce.ID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
if _, err := os.Stat(logfile); os.IsNotExist(err) {
return bytes.NewBufferString("logfile does not exist"), nil
}
logs, err := ioutil.ReadFile(logfile)
if err != nil {
return nil, err
}
ce.Status = tfe.CostEstimateFinished
return bytes.NewBuffer(logs), nil
}
// mockInput is a mock implementation of terraform.UIInput. // mockInput is a mock implementation of terraform.UIInput.
type mockInput struct { type mockInput struct {
answers map[string]string answers map[string]string
@ -647,6 +731,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
return nil, err return nil, err
} }
ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil {
return nil, err
}
p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -661,6 +750,7 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
ID: generateID("run-"), ID: generateID("run-"),
Actions: &tfe.RunActions{IsCancelable: true}, Actions: &tfe.RunActions{IsCancelable: true},
Apply: a, Apply: a,
CostEstimate: ce,
HasChanges: false, HasChanges: false,
Permissions: &tfe.RunPermissions{}, Permissions: &tfe.RunPermissions{},
Plan: p, Plan: p,
@ -960,6 +1050,14 @@ func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace strin
return w, nil return w, nil
} }
func (m *mockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
return w, nil
}
func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceNames[workspace] w, ok := m.workspaceNames[workspace]
if !ok { if !ok {
@ -982,6 +1080,28 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str
return w, nil return w, nil
} }
func (m *mockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
if options.Name != nil {
w.Name = *options.Name
}
if options.TerraformVersion != nil {
w.TerraformVersion = *options.TerraformVersion
}
if options.WorkingDirectory != nil {
w.WorkingDirectory = *options.WorkingDirectory
}
delete(m.workspaceNames, w.Name)
m.workspaceNames[w.Name] = w
return w, nil
}
func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
if w, ok := m.workspaceNames[workspace]; ok { if w, ok := m.workspaceNames[workspace]; ok {
delete(m.workspaceIDs, w.ID) delete(m.workspaceIDs, w.ID)
@ -990,6 +1110,14 @@ func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace str
return nil return nil
} }
func (m *mockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error {
if w, ok := m.workspaceIDs[workspaceID]; ok {
delete(m.workspaceIDs, w.Name)
}
delete(m.workspaceIDs, workspaceID)
return nil
}
func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
w, ok := m.workspaceNames[workspace] w, ok := m.workspaceNames[workspace]
if !ok { if !ok {
@ -999,6 +1127,15 @@ func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization,
return w, nil return w, nil
} }
func (m *mockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID]
if !ok {
return nil, tfe.ErrResourceNotFound
}
w.VCSRepo = nil
return w, nil
}
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
w, ok := m.workspaceIDs[workspaceID] w, ok := m.workspaceIDs[workspaceID]
if !ok { if !ok {

View File

@ -316,6 +316,14 @@ to capture the filesystem context the remote workspace expects:
return r, nil return r, nil
} }
// Show any cost estimation output.
if r.CostEstimate != nil {
err = b.costEstimate(stopCtx, cancelCtx, op, r)
if err != nil {
return r, err
}
}
// Check any configured sentinel policies. // Check any configured sentinel policies.
if len(r.PolicyChecks) > 0 { if len(r.PolicyChecks) > 0 {
err = b.checkPolicy(stopCtx, cancelCtx, op, r) err = b.checkPolicy(stopCtx, cancelCtx, op, r)

View File

@ -59,7 +59,7 @@ func TestRemote_planBasic(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output) t.Fatalf("expected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -113,7 +113,7 @@ func TestRemote_planLongLine(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output) t.Fatalf("expected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -374,7 +374,7 @@ func TestRemote_planNoChanges(t *testing.T) {
output := b.CLI.(*cli.MockUi).OutputWriter.String() output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
t.Fatalf("expected no changes in plan summery: %s", output) t.Fatalf("expected no changes in plan summary: %s", output)
} }
if !strings.Contains(output, "Sentinel Result: true") { if !strings.Contains(output, "Sentinel Result: true") {
t.Fatalf("expected policy check result in output: %s", output) t.Fatalf("expected policy check result in output: %s", output)
@ -415,7 +415,7 @@ func TestRemote_planForceLocal(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output) t.Fatalf("unexpected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -446,7 +446,7 @@ func TestRemote_planWithoutOperationsEntitlement(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output) t.Fatalf("unexpected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -491,7 +491,7 @@ func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
t.Fatalf("unexpected remote backend header in output: %s", output) t.Fatalf("unexpected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -562,7 +562,7 @@ func TestRemote_planLockTimeout(t *testing.T) {
t.Fatalf("expected lock timout error in output: %s", output) t.Fatalf("expected lock timout error in output: %s", output)
} }
if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("unexpected plan summery in output: %s", output) t.Fatalf("unexpected plan summary in output: %s", output)
} }
} }
@ -654,7 +654,7 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output) t.Fatalf("expected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -709,7 +709,41 @@ func TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) {
t.Fatalf("expected remote backend header in output: %s", output) t.Fatalf("expected remote backend header in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
}
}
func TestRemote_planCostEstimation(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
op, configCleanup := testOperationPlan(t, "./testdata/plan-cost-estimation")
defer configCleanup()
op.Workspace = backend.DefaultStateName
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
if run.Result != backend.OperationSuccess {
t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
}
if run.PlanEmpty {
t.Fatalf("expected a non-empty plan")
}
output := b.CLI.(*cli.MockUi).OutputWriter.String()
if !strings.Contains(output, "Running plan in the remote backend") {
t.Fatalf("expected remote backend header in output: %s", output)
}
if !strings.Contains(output, "Resources: 1 of 1 estimated") {
t.Fatalf("expected cost estimate result in output: %s", output)
}
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -743,7 +777,7 @@ func TestRemote_planPolicyPass(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output) t.Fatalf("expected policy check result in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -782,7 +816,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output) t.Fatalf("expected policy check result in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }
@ -821,7 +855,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
t.Fatalf("expected policy check result in output: %s", output) t.Fatalf("expected policy check result in output: %s", output)
} }
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
t.Fatalf("expected plan summery in output: %s", output) t.Fatalf("expected plan summary in output: %s", output)
} }
} }

View File

@ -0,0 +1,5 @@
Cost estimation:
Waiting for cost estimation to complete...
Resources: 1 of 1 estimated
$25.488/mo +$25.488

View File

@ -1,5 +1,4 @@
Terraform v0.11.7 Terraform v0.12.9
Configuring remote state backend... Configuring remote state backend...
Initializing Terraform configuration... Initializing Terraform configuration...
Refreshing Terraform state in-memory prior to plan... Refreshing Terraform state in-memory prior to plan...

View File

@ -115,6 +115,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
b.CLI = cli.NewMockUi() b.CLI = cli.NewMockUi()
b.client.Applies = mc.Applies b.client.Applies = mc.Applies
b.client.ConfigurationVersions = mc.ConfigurationVersions b.client.ConfigurationVersions = mc.ConfigurationVersions
b.client.CostEstimates = mc.CostEstimates
b.client.Organizations = mc.Organizations b.client.Organizations = mc.Organizations
b.client.Plans = mc.Plans b.client.Plans = mc.Plans
b.client.PolicyChecks = mc.PolicyChecks b.client.PolicyChecks = mc.PolicyChecks

2
go.mod
View File

@ -64,7 +64,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.5.2 github.com/hashicorp/go-retryablehttp v0.5.2
github.com/hashicorp/go-rootcerts v1.0.0 github.com/hashicorp/go-rootcerts v1.0.0
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
github.com/hashicorp/go-tfe v0.3.16 github.com/hashicorp/go-tfe v0.3.23
github.com/hashicorp/go-uuid v1.0.1 github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.1.0 github.com/hashicorp/go-version v1.1.0
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f

4
go.sum
View File

@ -198,8 +198,8 @@ github.com/hashicorp/go-slug v0.3.0 h1:L0c+AvH/J64iMNF4VqRaRku2DMTEuHioPVS7kMjWI
github.com/hashicorp/go-slug v0.3.0/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= github.com/hashicorp/go-slug v0.3.0/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 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-tfe v0.3.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0= github.com/hashicorp/go-tfe v0.3.23 h1:kd9hlFQvGubNF/CpF7T5AP/xU8uLUq8ANbI5xRDVSms=
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM= github.com/hashicorp/go-tfe v0.3.23/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=

View File

@ -136,6 +136,7 @@ tests:
$ export TFE_ADDRESS=https://tfe.local $ export TFE_ADDRESS=https://tfe.local
$ export TFE_TOKEN=xxxxxxxxxxxxxxxxxxx $ export TFE_TOKEN=xxxxxxxxxxxxxxxxxxx
$ export GITHUB_TOKEN=xxxxxxxxxxxxxxxx $ export GITHUB_TOKEN=xxxxxxxxxxxxxxxx
$ export GITHUB_IDENTIFIER=xxxxxxxxxxx
``` ```
In order for the tests relating to queuing and capacity to pass, FRQ should be In order for the tests relating to queuing and capacity to pass, FRQ should be

129
vendor/github.com/hashicorp/go-tfe/cost_estimate.go generated vendored Normal file
View File

@ -0,0 +1,129 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ CostEstimates = (*costEstimates)(nil)
// CostEstimates describes all the costEstimate related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/ (TBD)
type CostEstimates interface {
// Read a costEstimate by its ID.
Read(ctx context.Context, costEstimateID string) (*CostEstimate, error)
// Logs retrieves the logs of a costEstimate.
Logs(ctx context.Context, costEstimateID string) (io.Reader, error)
}
// costEstimates implements CostEstimates.
type costEstimates struct {
client *Client
}
// CostEstimateStatus represents a costEstimate state.
type CostEstimateStatus string
//List all available costEstimate statuses.
const (
CostEstimateCanceled CostEstimateStatus = "canceled"
CostEstimateErrored CostEstimateStatus = "errored"
CostEstimateFinished CostEstimateStatus = "finished"
CostEstimatePending CostEstimateStatus = "pending"
CostEstimateQueued CostEstimateStatus = "queued"
)
// CostEstimate represents a Terraform Enterprise costEstimate.
type CostEstimate struct {
ID string `jsonapi:"primary,cost-estimates"`
DeltaMonthlyCost string `jsonapi:"attr,delta-monthly-cost"`
ErrorMessage string `jsonapi:"attr,error-message"`
MatchedResourcesCount int `jsonapi:"attr,matched-resources-count"`
PriorMonthlyCost string `jsonapi:"attr,prior-monthly-cost"`
ProposedMonthlyCost string `jsonapi:"attr,proposed-monthly-cost"`
ResourcesCount int `jsonapi:"attr,resources-count"`
Status CostEstimateStatus `jsonapi:"attr,status"`
StatusTimestamps *CostEstimateStatusTimestamps `jsonapi:"attr,status-timestamps"`
UnmatchedResourcesCount int `jsonapi:"attr,unmatched-resources-count"`
}
// CostEstimateStatusTimestamps holds the timestamps for individual costEstimate statuses.
type CostEstimateStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
PendingAt time.Time `json:"pending-at"`
QueuedAt time.Time `json:"queued-at"`
}
// Read a costEstimate by its ID.
func (s *costEstimates) Read(ctx context.Context, costEstimateID string) (*CostEstimate, error) {
if !validStringID(&costEstimateID) {
return nil, errors.New("invalid value for cost estimate ID")
}
u := fmt.Sprintf("cost-estimates/%s", url.QueryEscape(costEstimateID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
ce := &CostEstimate{}
err = s.client.do(ctx, req, ce)
if err != nil {
return nil, err
}
return ce, nil
}
// Logs retrieves the logs of a costEstimate.
func (s *costEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) {
if !validStringID(&costEstimateID) {
return nil, errors.New("invalid value for cost estimate ID")
}
// Loop until the context is canceled or the cost estimate is finished
// running. The cost estimate logs are not streamed and so only available
// once the estimate is finished.
for {
// Get the costEstimate to make sure it exists.
ce, err := s.Read(ctx, costEstimateID)
if err != nil {
return nil, err
}
switch ce.Status {
case CostEstimateQueued:
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(1000 * time.Millisecond):
continue
}
}
u := fmt.Sprintf("cost-estimates/%s/output", url.QueryEscape(costEstimateID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
logs := bytes.NewBuffer(nil)
err = s.client.do(ctx, req, logs)
if err != nil {
return nil, err
}
return logs, nil
}
}

View File

@ -1,121 +0,0 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ CostEstimations = (*costEstimations)(nil)
// CostEstimations describes all the costEstimation related methods that
// the Terraform Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/ (TBD)
type CostEstimations interface {
// Read a costEstimation by its ID.
Read(ctx context.Context, costEstimationID string) (*CostEstimation, error)
// Logs retrieves the logs of a costEstimation.
Logs(ctx context.Context, costEstimationID string) (io.Reader, error)
}
// costEstimations implements CostEstimations.
type costEstimations struct {
client *Client
}
// CostEstimationStatus represents a costEstimation state.
type CostEstimationStatus string
//List all available costEstimation statuses.
const (
CostEstimationCanceled CostEstimationStatus = "canceled"
CostEstimationErrored CostEstimationStatus = "errored"
CostEstimationFinished CostEstimationStatus = "finished"
CostEstimationQueued CostEstimationStatus = "queued"
)
// CostEstimation represents a Terraform Enterprise costEstimation.
type CostEstimation struct {
ID string `jsonapi:"primary,cost-estimations"`
ErrorMessage string `jsonapi:"attr,error-message"`
Status CostEstimationStatus `jsonapi:"attr,status"`
StatusTimestamps *CostEstimationStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// CostEstimationStatusTimestamps holds the timestamps for individual costEstimation statuses.
type CostEstimationStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
FinishedAt time.Time `json:"finished-at"`
QueuedAt time.Time `json:"queued-at"`
}
// Read a costEstimation by its ID.
func (s *costEstimations) Read(ctx context.Context, costEstimationID string) (*CostEstimation, error) {
if !validStringID(&costEstimationID) {
return nil, errors.New("invalid value for cost estimation ID")
}
u := fmt.Sprintf("cost-estimations/%s", url.QueryEscape(costEstimationID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
ce := &CostEstimation{}
err = s.client.do(ctx, req, ce)
if err != nil {
return nil, err
}
return ce, nil
}
// Logs retrieves the logs of a costEstimation.
func (s *costEstimations) Logs(ctx context.Context, costEstimationID string) (io.Reader, error) {
if !validStringID(&costEstimationID) {
return nil, errors.New("invalid value for cost estimation ID")
}
// Loop until the context is canceled or the cost estimation is finished
// running. The cost estimation logs are not streamed and so only available
// once the estimation is finished.
for {
// Get the costEstimation to make sure it exists.
ce, err := s.Read(ctx, costEstimationID)
if err != nil {
return nil, err
}
switch ce.Status {
case CostEstimationQueued:
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(500 * time.Millisecond):
continue
}
}
u := fmt.Sprintf("cost-estimations/%s/output", url.QueryEscape(costEstimationID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
logs := bytes.NewBuffer(nil)
err = s.client.do(ctx, req, logs)
if err != nil {
return nil, err
}
return logs, nil
}
}

View File

@ -55,6 +55,9 @@ type Plan struct {
ResourceDestructions int `jsonapi:"attr,resource-destructions"` ResourceDestructions int `jsonapi:"attr,resource-destructions"`
Status PlanStatus `jsonapi:"attr,status"` Status PlanStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"` StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
// Relations
Exports []*PlanExport `jsonapi:"relation,exports"`
} }
// PlanStatusTimestamps holds the timestamps for individual plan statuses. // PlanStatusTimestamps holds the timestamps for individual plan statuses.

175
vendor/github.com/hashicorp/go-tfe/plan_export.go generated vendored Normal file
View File

@ -0,0 +1,175 @@
package tfe
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"time"
)
// Compile-time proof of interface implementation.
var _ PlanExports = (*planExports)(nil)
// PlanExports describes all the plan export related methods that the Terraform
// Enterprise API supports.
//
// TFE API docs: https://www.terraform.io/docs/enterprise/api/plan-exports.html
type PlanExports interface {
// Export a plan by its ID with the given options.
Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error)
// Read a plan export by its ID.
Read(ctx context.Context, planExportID string) (*PlanExport, error)
// Delete a plan export by its ID.
Delete(ctx context.Context, planExportID string) error
// Download the data of an plan export.
Download(ctx context.Context, planExportID string) ([]byte, error)
}
// planExports implements PlanExports.
type planExports struct {
client *Client
}
// PlanExportDataType represents the type of data exported from a plan.
type PlanExportDataType string
// List all available plan export data types.
const (
PlanExportSentinelMockBundleV0 PlanExportDataType = "sentinel-mock-bundle-v0"
)
// PlanExportStatus represents a plan export state.
type PlanExportStatus string
// List all available plan export statuses.
const (
PlanExportCanceled PlanExportStatus = "canceled"
PlanExportErrored PlanExportStatus = "errored"
PlanExportExpired PlanExportStatus = "expired"
PlanExportFinished PlanExportStatus = "finished"
PlanExportPending PlanExportStatus = "pending"
PlanExportQueued PlanExportStatus = "queued"
)
// PlanExportStatusTimestamps holds the timestamps for plan export statuses.
type PlanExportStatusTimestamps struct {
CanceledAt time.Time `json:"canceled-at"`
ErroredAt time.Time `json:"errored-at"`
ExpiredAt time.Time `json:"expired-at"`
FinishedAt time.Time `json:"finished-at"`
QueuedAt time.Time `json:"queued-at"`
}
// PlanExport represents an export of Terraform Enterprise plan data.
type PlanExport struct {
ID string `jsonapi:"primary,plan-exports"`
DataType PlanExportDataType `jsonapi:"attr,data-type"`
Status PlanExportStatus `jsonapi:"attr,status"`
StatusTimestamps *PlanExportStatusTimestamps `jsonapi:"attr,status-timestamps"`
}
// PlanExportCreateOptions represents the options for exporting data from a plan.
type PlanExportCreateOptions struct {
// For internal use only!
ID string `jsonapi:"primary,plan-exports"`
// The plan to export.
Plan *Plan `jsonapi:"relation,plan"`
// The name of the policy set.
DataType *PlanExportDataType `jsonapi:"attr,data-type"`
}
func (o PlanExportCreateOptions) valid() error {
if o.Plan == nil {
return errors.New("plan is required")
}
if o.DataType == nil {
return errors.New("data type is required")
}
return nil
}
func (s *planExports) Create(ctx context.Context, options PlanExportCreateOptions) (*PlanExport, error) {
if err := options.valid(); err != nil {
return nil, err
}
// Make sure we don't send a user provided ID.
options.ID = ""
req, err := s.client.newRequest("POST", "plan-exports", &options)
if err != nil {
return nil, err
}
pe := &PlanExport{}
err = s.client.do(ctx, req, pe)
if err != nil {
return nil, err
}
return pe, err
}
// Read a plan export by its ID.
func (s *planExports) Read(ctx context.Context, planExportID string) (*PlanExport, error) {
if !validStringID(&planExportID) {
return nil, errors.New("invalid value for plan export ID")
}
u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
pe := &PlanExport{}
err = s.client.do(ctx, req, pe)
if err != nil {
return nil, err
}
return pe, nil
}
// Delete a plan export by ID.
func (s *planExports) Delete(ctx context.Context, planExportID string) error {
if !validStringID(&planExportID) {
return errors.New("invalid value for plan export ID")
}
u := fmt.Sprintf("plan-exports/%s", url.QueryEscape(planExportID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// Download a plan export's data. Data is exported in a .tar.gz format.
func (s *planExports) Download(ctx context.Context, planExportID string) ([]byte, error) {
if !validStringID(&planExportID) {
return nil, errors.New("invalid value for plan export ID")
}
u := fmt.Sprintf("plan-exports/%s/download", url.QueryEscape(planExportID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
var buf bytes.Buffer
err = s.client.do(ctx, req, &buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -28,10 +28,12 @@ type PolicySets interface {
// Update an existing policy set. // Update an existing policy set.
Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error) Update(ctx context.Context, policySetID string, options PolicySetUpdateOptions) (*PolicySet, error)
// Add policies to a policy set. // Add policies to a policy set. This function can only be used when
// there is no VCS repository associated with the policy set.
AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error AddPolicies(ctx context.Context, policySetID string, options PolicySetAddPoliciesOptions) error
// Remove policies from a policy set. // Remove policies from a policy set. This function can only be used
// when there is no VCS repository associated with the policy set.
RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error RemovePolicies(ctx context.Context, policySetID string, options PolicySetRemovePoliciesOptions) error
// Add workspaces to a policy set. // Add workspaces to a policy set.
@ -61,7 +63,9 @@ type PolicySet struct {
Name string `jsonapi:"attr,name"` Name string `jsonapi:"attr,name"`
Description string `jsonapi:"attr,description"` Description string `jsonapi:"attr,description"`
Global bool `jsonapi:"attr,global"` Global bool `jsonapi:"attr,global"`
PoliciesPath string `jsonapi:"attr,policies-path"`
PolicyCount int `jsonapi:"attr,policy-count"` PolicyCount int `jsonapi:"attr,policy-count"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
WorkspaceCount int `jsonapi:"attr,workspace-count"` WorkspaceCount int `jsonapi:"attr,workspace-count"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"`
@ -115,9 +119,21 @@ type PolicySetCreateOptions struct {
// Whether or not the policy set is global. // Whether or not the policy set is global.
Global *bool `jsonapi:"attr,global,omitempty"` 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"`
// The initial members of the policy set. // The initial members of the policy set.
Policies []*Policy `jsonapi:"relation,policies,omitempty"` Policies []*Policy `jsonapi:"relation,policies,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
// this option is mutually exclusive with the Policies option and
// both cannot be used at the same time.
VCSRepo *VCSRepoOptions `jsonapi:"attr,vcs-repo,omitempty"`
// The initial list of workspaces for which the policy set should be enforced. // The initial list of workspaces for which the policy set should be enforced.
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"` Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
} }

View File

@ -102,7 +102,7 @@ type Run struct {
// Relations // Relations
Apply *Apply `jsonapi:"relation,apply"` Apply *Apply `jsonapi:"relation,apply"`
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"` ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
CostEstimation *CostEstimation `jsonapi:"relation,cost-estimation"` CostEstimate *CostEstimate `jsonapi:"relation,cost-estimate"`
Plan *Plan `jsonapi:"relation,plan"` Plan *Plan `jsonapi:"relation,plan"`
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"` PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
Workspace *Workspace `jsonapi:"relation,workspace"` Workspace *Workspace `jsonapi:"relation,workspace"`

View File

@ -32,6 +32,8 @@ const (
DefaultAddress = "https://app.terraform.io" DefaultAddress = "https://app.terraform.io"
// DefaultBasePath on which the API is served. // DefaultBasePath on which the API is served.
DefaultBasePath = "/api/v2/" DefaultBasePath = "/api/v2/"
// No-op API endpoint used to configure the rate limiter
PingEndpoint = "ping"
) )
var ( var (
@ -106,13 +108,14 @@ type Client struct {
Applies Applies Applies Applies
ConfigurationVersions ConfigurationVersions ConfigurationVersions ConfigurationVersions
CostEstimations CostEstimations CostEstimates CostEstimates
NotificationConfigurations NotificationConfigurations NotificationConfigurations NotificationConfigurations
OAuthClients OAuthClients OAuthClients OAuthClients
OAuthTokens OAuthTokens OAuthTokens OAuthTokens
Organizations Organizations Organizations Organizations
OrganizationTokens OrganizationTokens OrganizationTokens OrganizationTokens
Plans Plans Plans Plans
PlanExports PlanExports
Policies Policies Policies Policies
PolicyChecks PolicyChecks PolicyChecks PolicyChecks
PolicySets PolicySets PolicySets PolicySets
@ -196,13 +199,14 @@ func NewClient(cfg *Config) (*Client, error) {
// Create the services. // Create the services.
client.Applies = &applies{client: client} client.Applies = &applies{client: client}
client.ConfigurationVersions = &configurationVersions{client: client} client.ConfigurationVersions = &configurationVersions{client: client}
client.CostEstimations = &costEstimations{client: client} client.CostEstimates = &costEstimates{client: client}
client.NotificationConfigurations = &notificationConfigurations{client: client} client.NotificationConfigurations = &notificationConfigurations{client: client}
client.OAuthClients = &oAuthClients{client: client} client.OAuthClients = &oAuthClients{client: client}
client.OAuthTokens = &oAuthTokens{client: client} client.OAuthTokens = &oAuthTokens{client: client}
client.Organizations = &organizations{client: client} client.Organizations = &organizations{client: client}
client.OrganizationTokens = &organizationTokens{client: client} client.OrganizationTokens = &organizationTokens{client: client}
client.Plans = &plans{client: client} client.Plans = &plans{client: client}
client.PlanExports = &planExports{client: client}
client.Policies = &policies{client: client} client.Policies = &policies{client: client}
client.PolicyChecks = &policyChecks{client: client} client.PolicyChecks = &policyChecks{client: client}
client.PolicySets = &policySets{client: client} client.PolicySets = &policySets{client: client}
@ -291,7 +295,11 @@ func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Respons
// configureLimiter configures the rate limiter. // configureLimiter configures the rate limiter.
func (c *Client) configureLimiter() error { func (c *Client) configureLimiter() error {
// Create a new request. // Create a new request.
req, err := http.NewRequest("GET", c.baseURL.String(), nil) u, err := c.baseURL.Parse(PingEndpoint)
if err != nil {
return err
}
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -40,6 +40,11 @@ func NotificationDestination(v NotificationDestinationType) *NotificationDestina
return &v return &v
} }
// PlanExportType returns a pointer to the given plan export data type.
func PlanExportType(v PlanExportDataType) *PlanExportDataType {
return &v
}
// ServiceProvider returns a pointer to the given service provider type. // ServiceProvider returns a pointer to the given service provider type.
func ServiceProvider(v ServiceProviderType) *ServiceProviderType { func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
return &v return &v

View File

@ -25,15 +25,27 @@ type Workspaces interface {
// Read a workspace by its name. // Read a workspace by its name.
Read(ctx context.Context, organization string, workspace string) (*Workspace, error) Read(ctx context.Context, organization string, workspace string) (*Workspace, error)
// ReadByID reads a workspace by its ID.
ReadByID(ctx context.Context, workspaceID string) (*Workspace, error)
// Update settings of an existing workspace. // Update settings of an existing workspace.
Update(ctx context.Context, organization string, workspace string, options WorkspaceUpdateOptions) (*Workspace, error) Update(ctx context.Context, organization string, workspace string, options WorkspaceUpdateOptions) (*Workspace, error)
// UpdateByID updates the settings of an existing workspace.
UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error)
// Delete a workspace by its name. // Delete a workspace by its name.
Delete(ctx context.Context, organization string, workspace string) error Delete(ctx context.Context, organization string, workspace string) error
// DeleteByID deletes a workspace by its ID.
DeleteByID(ctx context.Context, workspaceID string) error
// RemoveVCSConnection from a workspace. // RemoveVCSConnection from a workspace.
RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*Workspace, error)
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error)
// Lock a workspace by its ID. // Lock a workspace by its ID.
Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error) Lock(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error)
@ -69,6 +81,7 @@ type Workspace struct {
CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"` CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Environment string `jsonapi:"attr,environment"` Environment string `jsonapi:"attr,environment"`
FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"`
Locked bool `jsonapi:"attr,locked"` Locked bool `jsonapi:"attr,locked"`
MigrationEnvironment string `jsonapi:"attr,migration-environment"` MigrationEnvironment string `jsonapi:"attr,migration-environment"`
Name string `jsonapi:"attr,name"` Name string `jsonapi:"attr,name"`
@ -76,6 +89,7 @@ type Workspace struct {
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"` Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
QueueAllRuns bool `jsonapi:"attr,queue-all-runs"` QueueAllRuns bool `jsonapi:"attr,queue-all-runs"`
TerraformVersion string `jsonapi:"attr,terraform-version"` TerraformVersion string `jsonapi:"attr,terraform-version"`
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes"`
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"` VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
WorkingDirectory string `jsonapi:"attr,working-directory"` WorkingDirectory string `jsonapi:"attr,working-directory"`
@ -149,6 +163,12 @@ type WorkspaceCreateOptions struct {
// Whether to automatically apply changes when a Terraform plan is successful. // Whether to automatically apply changes when a Terraform plan is successful.
AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"` AutoApply *bool `jsonapi:"attr,auto-apply,omitempty"`
// Whether to filter runs based on the changed files in a VCS push. If
// enabled, the working directory and trigger prefixes describe a set of
// paths which must contain changes for a VCS push to trigger a run. If
// disabled, any push will trigger a run.
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
// The legacy TFE environment to use as the source of the migration, in the // The legacy TFE environment to use as the source of the migration, in the
// form organization/environment. Omit this unless you are migrating a legacy // form organization/environment. Omit this unless you are migrating a legacy
// environment. // environment.
@ -167,6 +187,10 @@ type WorkspaceCreateOptions struct {
// workspace, the latest version is selected unless otherwise specified. // workspace, the latest version is selected unless otherwise specified.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"` TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// List of repository-root-relative paths which list all locations to be
// tracked for changes. See FileTriggersEnabled above for more details.
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
// Settings for the workspace's VCS repository. If omitted, the workspace is // Settings for the workspace's VCS repository. If omitted, the workspace is
// created without a VCS repo. If included, you must specify at least the // created without a VCS repo. If included, you must specify at least the
// oauth-token-id and identifier keys below. // oauth-token-id and identifier keys below.
@ -251,6 +275,27 @@ func (s *workspaces) Read(ctx context.Context, organization, workspace string) (
return w, nil return w, nil
} }
// ReadByID reads a workspace by its ID.
func (s *workspaces) ReadByID(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// WorkspaceUpdateOptions represents the options for updating a workspace. // WorkspaceUpdateOptions represents the options for updating a workspace.
type WorkspaceUpdateOptions struct { type WorkspaceUpdateOptions struct {
// For internal use only! // For internal use only!
@ -265,6 +310,12 @@ type WorkspaceUpdateOptions struct {
// API and UI. // API and UI.
Name *string `jsonapi:"attr,name,omitempty"` Name *string `jsonapi:"attr,name,omitempty"`
// Whether to filter runs based on the changed files in a VCS push. If
// enabled, the working directory and trigger prefixes describe a set of
// paths which must contain changes for a VCS push to trigger a run. If
// disabled, any push will trigger a run.
FileTriggersEnabled *bool `jsonapi:"attr,file-triggers-enabled,omitempty"`
// Whether to queue all runs. Unless this is set to true, runs triggered by // Whether to queue all runs. Unless this is set to true, runs triggered by
// a webhook will not be queued until at least one run is manually queued. // a webhook will not be queued until at least one run is manually queued.
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"` QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
@ -272,6 +323,10 @@ type WorkspaceUpdateOptions struct {
// The version of Terraform to use for this workspace. // The version of Terraform to use for this workspace.
TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"` TerraformVersion *string `jsonapi:"attr,terraform-version,omitempty"`
// List of repository-root-relative paths which list all locations to be
// tracked for changes. See FileTriggersEnabled above for more details.
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes,omitempty"`
// To delete a workspace's existing VCS repo, specify null instead of an // To delete a workspace's existing VCS repo, specify null instead of an
// object. To modify a workspace's existing VCS repo, include whichever of // object. To modify a workspace's existing VCS repo, include whichever of
// the keys below you wish to modify. To add a new VCS repo to a workspace // the keys below you wish to modify. To add a new VCS repo to a workspace
@ -317,6 +372,30 @@ func (s *workspaces) Update(ctx context.Context, organization, workspace string,
return w, nil return w, nil
} }
// UpdateByID updates the settings of an existing workspace.
func (s *workspaces) UpdateByID(ctx context.Context, workspaceID string, options WorkspaceUpdateOptions) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("invalid value for workspace ID")
}
// Make sure we don't send a user provided ID.
options.ID = ""
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// Delete a workspace by its name. // Delete a workspace by its name.
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error { func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
if !validStringID(&organization) { if !validStringID(&organization) {
@ -339,6 +418,21 @@ func (s *workspaces) Delete(ctx context.Context, organization, workspace string)
return s.client.do(ctx, req, nil) return s.client.do(ctx, req, nil)
} }
// DeleteByID deletes a workspace by its ID.
func (s *workspaces) DeleteByID(ctx context.Context, workspaceID string) error {
if !validStringID(&workspaceID) {
return errors.New("invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}
return s.client.do(ctx, req, nil)
}
// workspaceRemoveVCSConnectionOptions // workspaceRemoveVCSConnectionOptions
type workspaceRemoveVCSConnectionOptions struct { type workspaceRemoveVCSConnectionOptions struct {
ID string `jsonapi:"primary,workspaces"` ID string `jsonapi:"primary,workspaces"`
@ -374,6 +468,28 @@ func (s *workspaces) RemoveVCSConnection(ctx context.Context, organization, work
return w, nil return w, nil
} }
// RemoveVCSConnectionByID removes a VCS connection from a workspace.
func (s *workspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*Workspace, error) {
if !validStringID(&workspaceID) {
return nil, errors.New("invalid value for workspace ID")
}
u := fmt.Sprintf("workspaces/%s", url.QueryEscape(workspaceID))
req, err := s.client.newRequest("PATCH", u, &workspaceRemoveVCSConnectionOptions{})
if err != nil {
return nil, err
}
w := &Workspace{}
err = s.client.do(ctx, req, w)
if err != nil {
return nil, err
}
return w, nil
}
// WorkspaceLockOptions represents the options for locking a workspace. // WorkspaceLockOptions represents the options for locking a workspace.
type WorkspaceLockOptions struct { type WorkspaceLockOptions struct {
// Specifies the reason for locking the workspace. // Specifies the reason for locking the workspace.

2
vendor/modules.txt vendored
View File

@ -315,7 +315,7 @@ github.com/hashicorp/go-rootcerts
github.com/hashicorp/go-safetemp github.com/hashicorp/go-safetemp
# github.com/hashicorp/go-slug v0.3.0 # github.com/hashicorp/go-slug v0.3.0
github.com/hashicorp/go-slug github.com/hashicorp/go-slug
# github.com/hashicorp/go-tfe v0.3.16 # github.com/hashicorp/go-tfe v0.3.23
github.com/hashicorp/go-tfe github.com/hashicorp/go-tfe
# github.com/hashicorp/go-uuid v1.0.1 # github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-uuid github.com/hashicorp/go-uuid