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"
|
"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")
|
||||||
|
|
|
@ -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
|
||||||
|
@ -658,13 +747,14 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &tfe.Run{
|
r := &tfe.Run{
|
||||||
ID: generateID("run-"),
|
ID: generateID("run-"),
|
||||||
Actions: &tfe.RunActions{IsCancelable: true},
|
Actions: &tfe.RunActions{IsCancelable: true},
|
||||||
Apply: a,
|
Apply: a,
|
||||||
HasChanges: false,
|
CostEstimate: ce,
|
||||||
Permissions: &tfe.RunPermissions{},
|
HasChanges: false,
|
||||||
Plan: p,
|
Permissions: &tfe.RunPermissions{},
|
||||||
Status: tfe.RunPending,
|
Plan: p,
|
||||||
|
Status: tfe.RunPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
if pc != nil {
|
if pc != nil {
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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...
|
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...
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
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-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=
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"`
|
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.
|
||||||
|
|
|
@ -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 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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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 = ¬ificationConfigurations{client: client}
|
client.NotificationConfigurations = ¬ificationConfigurations{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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue