Merge pull request #22761 from hashicorp/pault/tfce-ga
Remote Backend: Support latest cost-estimate API
This commit is contained in:
commit
6f313abc9e
|
@ -7,6 +7,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
|
@ -227,6 +229,87 @@ func (b *Remote) parseVariableValues(op *backend.Operation) (terraform.InputValu
|
|||
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 {
|
||||
if b.CLI != nil {
|
||||
b.CLI.Output("\n------------------------------------------------------------------------\n")
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
type mockClient struct {
|
||||
Applies *mockApplies
|
||||
ConfigurationVersions *mockConfigurationVersions
|
||||
CostEstimates *mockCostEstimates
|
||||
Organizations *mockOrganizations
|
||||
Plans *mockPlans
|
||||
PolicyChecks *mockPolicyChecks
|
||||
|
@ -33,6 +34,7 @@ func newMockClient() *mockClient {
|
|||
c := &mockClient{}
|
||||
c.Applies = newMockApplies(c)
|
||||
c.ConfigurationVersions = newMockConfigurationVersions(c)
|
||||
c.CostEstimates = newMockCostEstimates(c)
|
||||
c.Organizations = newMockOrganizations(c)
|
||||
c.Plans = newMockPlans(c)
|
||||
c.PolicyChecks = newMockPolicyChecks(c)
|
||||
|
@ -212,6 +214,88 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
|
|||
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.
|
||||
type mockInput struct {
|
||||
answers map[string]string
|
||||
|
@ -647,6 +731,11 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -658,13 +747,14 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
|||
}
|
||||
|
||||
r := &tfe.Run{
|
||||
ID: generateID("run-"),
|
||||
Actions: &tfe.RunActions{IsCancelable: true},
|
||||
Apply: a,
|
||||
HasChanges: false,
|
||||
Permissions: &tfe.RunPermissions{},
|
||||
Plan: p,
|
||||
Status: tfe.RunPending,
|
||||
ID: generateID("run-"),
|
||||
Actions: &tfe.RunActions{IsCancelable: true},
|
||||
Apply: a,
|
||||
CostEstimate: ce,
|
||||
HasChanges: false,
|
||||
Permissions: &tfe.RunPermissions{},
|
||||
Plan: p,
|
||||
Status: tfe.RunPending,
|
||||
}
|
||||
|
||||
if pc != nil {
|
||||
|
@ -960,6 +1050,14 @@ func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace strin
|
|||
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) {
|
||||
w, ok := m.workspaceNames[workspace]
|
||||
if !ok {
|
||||
|
@ -982,6 +1080,28 @@ func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace str
|
|||
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 {
|
||||
if w, ok := m.workspaceNames[workspace]; ok {
|
||||
delete(m.workspaceIDs, w.ID)
|
||||
|
@ -990,6 +1110,14 @@ func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace str
|
|||
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) {
|
||||
w, ok := m.workspaceNames[workspace]
|
||||
if !ok {
|
||||
|
@ -999,6 +1127,15 @@ func (m *mockWorkspaces) RemoveVCSConnection(ctx context.Context, organization,
|
|||
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) {
|
||||
w, ok := m.workspaceIDs[workspaceID]
|
||||
if !ok {
|
||||
|
|
|
@ -316,6 +316,14 @@ to capture the filesystem context the remote workspace expects:
|
|||
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.
|
||||
if len(r.PolicyChecks) > 0 {
|
||||
err = b.checkPolicy(stopCtx, cancelCtx, op, r)
|
||||
|
|
|
@ -59,7 +59,7 @@ func TestRemote_planBasic(t *testing.T) {
|
|||
t.Fatalf("expected remote backend header in output: %s", output)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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()
|
||||
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") {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Cost estimation:
|
||||
|
||||
Waiting for cost estimation to complete...
|
||||
Resources: 1 of 1 estimated
|
||||
$25.488/mo +$25.488
|
|
@ -1,5 +1,4 @@
|
|||
Terraform v0.11.7
|
||||
|
||||
Terraform v0.12.9
|
||||
Configuring remote state backend...
|
||||
Initializing Terraform configuration...
|
||||
Refreshing Terraform state in-memory prior to plan...
|
||||
|
|
|
@ -115,6 +115,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) {
|
|||
b.CLI = cli.NewMockUi()
|
||||
b.client.Applies = mc.Applies
|
||||
b.client.ConfigurationVersions = mc.ConfigurationVersions
|
||||
b.client.CostEstimates = mc.CostEstimates
|
||||
b.client.Organizations = mc.Organizations
|
||||
b.client.Plans = mc.Plans
|
||||
b.client.PolicyChecks = mc.PolicyChecks
|
||||
|
|
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.16
|
||||
github.com/hashicorp/go-tfe v0.3.23
|
||||
github.com/hashicorp/go-uuid v1.0.1
|
||||
github.com/hashicorp/go-version v1.1.0
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||
|
|
4
go.sum
4
go.sum
|
@ -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-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.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0=
|
||||
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
|
||||
github.com/hashicorp/go-tfe v0.3.23 h1:kd9hlFQvGubNF/CpF7T5AP/xU8uLUq8ANbI5xRDVSms=
|
||||
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.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
|
|
|
@ -136,6 +136,7 @@ tests:
|
|||
$ export TFE_ADDRESS=https://tfe.local
|
||||
$ export TFE_TOKEN=xxxxxxxxxxxxxxxxxxx
|
||||
$ export GITHUB_TOKEN=xxxxxxxxxxxxxxxx
|
||||
$ export GITHUB_IDENTIFIER=xxxxxxxxxxx
|
||||
```
|
||||
|
||||
In order for the tests relating to queuing and capacity to pass, FRQ should be
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -55,6 +55,9 @@ type Plan struct {
|
|||
ResourceDestructions int `jsonapi:"attr,resource-destructions"`
|
||||
Status PlanStatus `jsonapi:"attr,status"`
|
||||
StatusTimestamps *PlanStatusTimestamps `jsonapi:"attr,status-timestamps"`
|
||||
|
||||
// Relations
|
||||
Exports []*PlanExport `jsonapi:"relation,exports"`
|
||||
}
|
||||
|
||||
// PlanStatusTimestamps holds the timestamps for individual plan statuses.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -28,10 +28,12 @@ type PolicySets interface {
|
|||
// Update an existing policy set.
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
// Add workspaces to a policy set.
|
||||
|
@ -61,7 +63,9 @@ type PolicySet struct {
|
|||
Name string `jsonapi:"attr,name"`
|
||||
Description string `jsonapi:"attr,description"`
|
||||
Global bool `jsonapi:"attr,global"`
|
||||
PoliciesPath string `jsonapi:"attr,policies-path"`
|
||||
PolicyCount int `jsonapi:"attr,policy-count"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
WorkspaceCount int `jsonapi:"attr,workspace-count"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-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.
|
||||
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.
|
||||
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.
|
||||
Workspaces []*Workspace `jsonapi:"relation,workspaces,omitempty"`
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ type Run struct {
|
|||
// Relations
|
||||
Apply *Apply `jsonapi:"relation,apply"`
|
||||
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
|
||||
CostEstimation *CostEstimation `jsonapi:"relation,cost-estimation"`
|
||||
CostEstimate *CostEstimate `jsonapi:"relation,cost-estimate"`
|
||||
Plan *Plan `jsonapi:"relation,plan"`
|
||||
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
|
||||
Workspace *Workspace `jsonapi:"relation,workspace"`
|
||||
|
|
|
@ -32,6 +32,8 @@ const (
|
|||
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 = "ping"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -106,13 +108,14 @@ type Client struct {
|
|||
|
||||
Applies Applies
|
||||
ConfigurationVersions ConfigurationVersions
|
||||
CostEstimations CostEstimations
|
||||
CostEstimates CostEstimates
|
||||
NotificationConfigurations NotificationConfigurations
|
||||
OAuthClients OAuthClients
|
||||
OAuthTokens OAuthTokens
|
||||
Organizations Organizations
|
||||
OrganizationTokens OrganizationTokens
|
||||
Plans Plans
|
||||
PlanExports PlanExports
|
||||
Policies Policies
|
||||
PolicyChecks PolicyChecks
|
||||
PolicySets PolicySets
|
||||
|
@ -196,13 +199,14 @@ func NewClient(cfg *Config) (*Client, error) {
|
|||
// Create the services.
|
||||
client.Applies = &applies{client: client}
|
||||
client.ConfigurationVersions = &configurationVersions{client: client}
|
||||
client.CostEstimations = &costEstimations{client: client}
|
||||
client.CostEstimates = &costEstimates{client: client}
|
||||
client.NotificationConfigurations = ¬ificationConfigurations{client: client}
|
||||
client.OAuthClients = &oAuthClients{client: client}
|
||||
client.OAuthTokens = &oAuthTokens{client: client}
|
||||
client.Organizations = &organizations{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.PolicySets = &policySets{client: client}
|
||||
|
@ -291,7 +295,11 @@ func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Respons
|
|||
// configureLimiter configures the rate limiter.
|
||||
func (c *Client) configureLimiter() error {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -40,6 +40,11 @@ func NotificationDestination(v NotificationDestinationType) *NotificationDestina
|
|||
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.
|
||||
func ServiceProvider(v ServiceProviderType) *ServiceProviderType {
|
||||
return &v
|
||||
|
|
|
@ -25,15 +25,27 @@ type Workspaces interface {
|
|||
// Read a workspace by its name.
|
||||
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(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(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(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(ctx context.Context, workspaceID string, options WorkspaceLockOptions) (*Workspace, error)
|
||||
|
||||
|
@ -69,6 +81,7 @@ type Workspace struct {
|
|||
CanQueueDestroyPlan bool `jsonapi:"attr,can-queue-destroy-plan"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||
Environment string `jsonapi:"attr,environment"`
|
||||
FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"`
|
||||
Locked bool `jsonapi:"attr,locked"`
|
||||
MigrationEnvironment string `jsonapi:"attr,migration-environment"`
|
||||
Name string `jsonapi:"attr,name"`
|
||||
|
@ -76,6 +89,7 @@ type Workspace struct {
|
|||
Permissions *WorkspacePermissions `jsonapi:"attr,permissions"`
|
||||
QueueAllRuns bool `jsonapi:"attr,queue-all-runs"`
|
||||
TerraformVersion string `jsonapi:"attr,terraform-version"`
|
||||
TriggerPrefixes []string `jsonapi:"attr,trigger-prefixes"`
|
||||
VCSRepo *VCSRepo `jsonapi:"attr,vcs-repo"`
|
||||
WorkingDirectory string `jsonapi:"attr,working-directory"`
|
||||
|
||||
|
@ -149,6 +163,12 @@ type WorkspaceCreateOptions struct {
|
|||
// Whether to automatically apply changes when a Terraform plan is successful.
|
||||
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
|
||||
// form organization/environment. Omit this unless you are migrating a legacy
|
||||
// environment.
|
||||
|
@ -167,6 +187,10 @@ type WorkspaceCreateOptions struct {
|
|||
// workspace, the latest version is selected unless otherwise specified.
|
||||
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
|
||||
// created without a VCS repo. If included, you must specify at least the
|
||||
// oauth-token-id and identifier keys below.
|
||||
|
@ -251,6 +275,27 @@ func (s *workspaces) Read(ctx context.Context, organization, workspace string) (
|
|||
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.
|
||||
type WorkspaceUpdateOptions struct {
|
||||
// For internal use only!
|
||||
|
@ -265,6 +310,12 @@ type WorkspaceUpdateOptions struct {
|
|||
// API and UI.
|
||||
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
|
||||
// a webhook will not be queued until at least one run is manually queued.
|
||||
QueueAllRuns *bool `jsonapi:"attr,queue-all-runs,omitempty"`
|
||||
|
@ -272,6 +323,10 @@ type WorkspaceUpdateOptions struct {
|
|||
// The version of Terraform to use for this workspace.
|
||||
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
|
||||
// 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
|
||||
|
@ -317,6 +372,30 @@ func (s *workspaces) Update(ctx context.Context, organization, workspace string,
|
|||
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.
|
||||
func (s *workspaces) Delete(ctx context.Context, organization, workspace string) error {
|
||||
if !validStringID(&organization) {
|
||||
|
@ -339,6 +418,21 @@ func (s *workspaces) Delete(ctx context.Context, organization, workspace string)
|
|||
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
|
||||
type workspaceRemoveVCSConnectionOptions struct {
|
||||
ID string `jsonapi:"primary,workspaces"`
|
||||
|
@ -374,6 +468,28 @@ func (s *workspaces) RemoveVCSConnection(ctx context.Context, organization, work
|
|||
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.
|
||||
type WorkspaceLockOptions struct {
|
||||
// Specifies the reason for locking the workspace.
|
||||
|
|
|
@ -315,7 +315,7 @@ github.com/hashicorp/go-rootcerts
|
|||
github.com/hashicorp/go-safetemp
|
||||
# github.com/hashicorp/go-slug v0.3.0
|
||||
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-uuid v1.0.1
|
||||
github.com/hashicorp/go-uuid
|
||||
|
|
Loading…
Reference in New Issue