Merge pull request #21102 from hashicorp/pault/remote-backend-go-tfe-update
update to latest go-tfe
This commit is contained in:
commit
6183ca44c8
|
@ -227,6 +227,57 @@ func (b *Remote) parseVariableValues(op *backend.Operation) (terraform.InputValu
|
||||||
return result, diags
|
return result, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Remote) costEstimation(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error {
|
||||||
|
if r.CostEstimation == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.CLI != nil {
|
||||||
|
b.CLI.Output("\n------------------------------------------------------------------------\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := b.client.CostEstimations.Logs(stopCtx, r.CostEstimation.ID)
|
||||||
|
if err != nil {
|
||||||
|
return generalError("Failed to retrieve cost estimation logs", err)
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(logs)
|
||||||
|
|
||||||
|
// Retrieve the cost estimation to get its current status.
|
||||||
|
ce, err := b.client.CostEstimations.Read(stopCtx, r.CostEstimation.ID)
|
||||||
|
if err != nil {
|
||||||
|
return generalError("Failed to retrieve cost estimation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgPrefix := "Cost estimation"
|
||||||
|
if b.CLI != nil {
|
||||||
|
b.CLI.Output(b.Colorize().Color(msgPrefix + ":\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
if b.CLI != nil {
|
||||||
|
b.CLI.Output(b.Colorize().Color(scanner.Text()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return generalError("Failed to read logs", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ce.Status {
|
||||||
|
case tfe.CostEstimationFinished:
|
||||||
|
if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply && b.CLI != nil {
|
||||||
|
b.CLI.Output("\n------------------------------------------------------------------------")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case tfe.CostEstimationErrored:
|
||||||
|
return fmt.Errorf(msgPrefix + " errored.")
|
||||||
|
case tfe.CostEstimationCanceled:
|
||||||
|
return fmt.Errorf(msgPrefix + " canceled.")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown or unexpected cost estimation state: %s", ce.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
CostEstimations *mockCostEstimations
|
||||||
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.CostEstimations = newMockCostEstimations(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,84 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockCostEstimations struct {
|
||||||
|
client *mockClient
|
||||||
|
estimations map[string]*tfe.CostEstimation
|
||||||
|
logs map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockCostEstimations(client *mockClient) *mockCostEstimations {
|
||||||
|
return &mockCostEstimations{
|
||||||
|
client: client,
|
||||||
|
estimations: make(map[string]*tfe.CostEstimation),
|
||||||
|
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 *mockCostEstimations) create(cvID, workspaceID string) (*tfe.CostEstimation, error) {
|
||||||
|
id := generateID("ce-")
|
||||||
|
|
||||||
|
ce := &tfe.CostEstimation{
|
||||||
|
ID: id,
|
||||||
|
Status: tfe.CostEstimationQueued,
|
||||||
|
}
|
||||||
|
|
||||||
|
w, ok := m.client.Workspaces.workspaceIDs[workspaceID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logfile := filepath.Join(
|
||||||
|
m.client.ConfigurationVersions.uploadPaths[cvID],
|
||||||
|
w.WorkingDirectory,
|
||||||
|
"ce.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 *mockCostEstimations) Read(ctx context.Context, costEstimationID string) (*tfe.CostEstimation, error) {
|
||||||
|
ce, ok := m.estimations[costEstimationID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return ce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCostEstimations) Logs(ctx context.Context, costEstimationID string) (io.Reader, error) {
|
||||||
|
ce, ok := m.estimations[costEstimationID]
|
||||||
|
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.CostEstimationFinished
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -652,19 +732,25 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ce, err := m.client.CostEstimations.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
CostEstimation: 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 {
|
||||||
|
|
|
@ -290,6 +290,14 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation,
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show any cost estimation output.
|
||||||
|
if r.CostEstimation != nil {
|
||||||
|
err = b.costEstimation(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)
|
||||||
|
|
|
@ -655,6 +655,40 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemote_costEstimation(t *testing.T) {
|
||||||
|
b, bCleanup := testBackendDefault(t)
|
||||||
|
defer bCleanup()
|
||||||
|
|
||||||
|
op, configCleanup := testOperationPlan(t, "./test-fixtures/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, "SKU") {
|
||||||
|
t.Fatalf("expected cost estimation 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemote_planPolicyPass(t *testing.T) {
|
func TestRemote_planPolicyPass(t *testing.T) {
|
||||||
b, bCleanup := testBackendDefault(t)
|
b, bCleanup := testBackendDefault(t)
|
||||||
defer bCleanup()
|
defer bCleanup()
|
||||||
|
@ -681,12 +715,12 @@ func TestRemote_planPolicyPass(t *testing.T) {
|
||||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||||
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") {
|
|
||||||
t.Fatalf("expected plan summery in output: %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)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||||
|
t.Fatalf("expected plan summery in output: %s", output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_planPolicyHardFail(t *testing.T) {
|
func TestRemote_planPolicyHardFail(t *testing.T) {
|
||||||
|
@ -720,12 +754,12 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
|
||||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||||
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") {
|
|
||||||
t.Fatalf("expected plan summery in output: %s", output)
|
|
||||||
}
|
|
||||||
if !strings.Contains(output, "Sentinel Result: false") {
|
if !strings.Contains(output, "Sentinel Result: false") {
|
||||||
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") {
|
||||||
|
t.Fatalf("expected plan summery in output: %s", output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_planPolicySoftFail(t *testing.T) {
|
func TestRemote_planPolicySoftFail(t *testing.T) {
|
||||||
|
@ -759,12 +793,12 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
|
||||||
if !strings.Contains(output, "Running plan in the remote backend") {
|
if !strings.Contains(output, "Running plan in the remote backend") {
|
||||||
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") {
|
|
||||||
t.Fatalf("expected plan summery in output: %s", output)
|
|
||||||
}
|
|
||||||
if !strings.Contains(output, "Sentinel Result: false") {
|
if !strings.Contains(output, "Sentinel Result: false") {
|
||||||
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") {
|
||||||
|
t.Fatalf("expected plan summery in output: %s", output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemote_planWithRemoteError(t *testing.T) {
|
func TestRemote_planWithRemoteError(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
+---------+------+-----+-------------+----------------------+
|
||||||
|
| PRODUCT | NAME | SKU | DESCRIPTION | DELTA |
|
||||||
|
+---------+------+-----+-------------+----------------------+
|
||||||
|
+---------+------+-----+-------------+----------------------+
|
||||||
|
| TOTAL | $0.000 USD / 720 HRS |
|
||||||
|
+---------+------+-----+-------------+----------------------+
|
|
@ -0,0 +1 @@
|
||||||
|
resource "null_resource" "foo" {}
|
|
@ -0,0 +1,21 @@
|
||||||
|
Terraform v0.11.7
|
||||||
|
|
||||||
|
Configuring remote state backend...
|
||||||
|
Initializing Terraform configuration...
|
||||||
|
Refreshing Terraform state in-memory prior to plan...
|
||||||
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
An execution plan has been generated and is shown below.
|
||||||
|
Resource actions are indicated with the following symbols:
|
||||||
|
+ create
|
||||||
|
|
||||||
|
Terraform will perform the following actions:
|
||||||
|
|
||||||
|
+ null_resource.foo
|
||||||
|
id: <computed>
|
||||||
|
|
||||||
|
|
||||||
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
|
@ -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.CostEstimations = mc.CostEstimations
|
||||||
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
|
@ -55,7 +55,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.14
|
github.com/hashicorp/go-tfe v0.3.16
|
||||||
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/golang-lru v0.5.0 // indirect
|
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -191,8 +191,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.14 h1:1eWmq4RAICGufydNUWu7ahb0gtq24pN9jatD2FkdxdE=
|
github.com/hashicorp/go-tfe v0.3.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0=
|
||||||
github.com/hashicorp/go-tfe v0.3.14/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
|
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
|
||||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
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=
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,7 @@ type OrganizationList struct {
|
||||||
type Organization struct {
|
type Organization struct {
|
||||||
Name string `jsonapi:"primary,organizations"`
|
Name string `jsonapi:"primary,organizations"`
|
||||||
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
|
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
|
||||||
|
CostEstimationEnabled bool `jsonapi:"attr,cost-estimation-enabled"`
|
||||||
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
|
||||||
Email string `jsonapi:"attr,email"`
|
Email string `jsonapi:"attr,email"`
|
||||||
EnterprisePlan EnterprisePlanType `jsonapi:"attr,enterprise-plan"`
|
EnterprisePlan EnterprisePlanType `jsonapi:"attr,enterprise-plan"`
|
||||||
|
@ -167,6 +168,9 @@ type OrganizationCreateOptions struct {
|
||||||
// Authentication policy.
|
// Authentication policy.
|
||||||
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
||||||
|
|
||||||
|
// Enable Cost Estimation
|
||||||
|
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
|
||||||
|
|
||||||
// The name of the "owners" team
|
// The name of the "owners" team
|
||||||
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -248,6 +252,9 @@ type OrganizationUpdateOptions struct {
|
||||||
// Authentication policy.
|
// Authentication policy.
|
||||||
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
CollaboratorAuthPolicy *AuthPolicyType `jsonapi:"attr,collaborator-auth-policy,omitempty"`
|
||||||
|
|
||||||
|
// Enable Cost Estimation
|
||||||
|
CostEstimationEnabled *bool `jsonapi:"attr,cost-estimation-enabled,omitempty"`
|
||||||
|
|
||||||
// The name of the "owners" team
|
// The name of the "owners" team
|
||||||
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,12 +49,16 @@ type RunStatus string
|
||||||
//List all available run statuses.
|
//List all available run statuses.
|
||||||
const (
|
const (
|
||||||
RunApplied RunStatus = "applied"
|
RunApplied RunStatus = "applied"
|
||||||
|
RunApplyQueued RunStatus = "apply_queued"
|
||||||
RunApplying RunStatus = "applying"
|
RunApplying RunStatus = "applying"
|
||||||
RunCanceled RunStatus = "canceled"
|
RunCanceled RunStatus = "canceled"
|
||||||
RunConfirmed RunStatus = "confirmed"
|
RunConfirmed RunStatus = "confirmed"
|
||||||
|
RunCostEstimated RunStatus = "cost_estimated"
|
||||||
|
RunCostEstimating RunStatus = "cost_estimating"
|
||||||
RunDiscarded RunStatus = "discarded"
|
RunDiscarded RunStatus = "discarded"
|
||||||
RunErrored RunStatus = "errored"
|
RunErrored RunStatus = "errored"
|
||||||
RunPending RunStatus = "pending"
|
RunPending RunStatus = "pending"
|
||||||
|
RunPlanQueued RunStatus = "plan_queued"
|
||||||
RunPlanned RunStatus = "planned"
|
RunPlanned RunStatus = "planned"
|
||||||
RunPlannedAndFinished RunStatus = "planned_and_finished"
|
RunPlannedAndFinished RunStatus = "planned_and_finished"
|
||||||
RunPlanning RunStatus = "planning"
|
RunPlanning RunStatus = "planning"
|
||||||
|
@ -98,6 +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"`
|
||||||
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"`
|
||||||
|
@ -274,7 +279,7 @@ func (s *runs) Cancel(ctx context.Context, runID string, options RunCancelOption
|
||||||
return s.client.do(ctx, req, nil)
|
return s.client.do(ctx, req, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunCancelOptions represents the options for force-canceling a run.
|
// RunForceCancelOptions represents the options for force-canceling a run.
|
||||||
type RunForceCancelOptions struct {
|
type RunForceCancelOptions struct {
|
||||||
// An optional comment explaining the reason for the force-cancel.
|
// An optional comment explaining the reason for the force-cancel.
|
||||||
Comment *string `json:"comment,omitempty"`
|
Comment *string `json:"comment,omitempty"`
|
||||||
|
|
|
@ -106,6 +106,7 @@ type Client struct {
|
||||||
|
|
||||||
Applies Applies
|
Applies Applies
|
||||||
ConfigurationVersions ConfigurationVersions
|
ConfigurationVersions ConfigurationVersions
|
||||||
|
CostEstimations CostEstimations
|
||||||
NotificationConfigurations NotificationConfigurations
|
NotificationConfigurations NotificationConfigurations
|
||||||
OAuthClients OAuthClients
|
OAuthClients OAuthClients
|
||||||
OAuthTokens OAuthTokens
|
OAuthTokens OAuthTokens
|
||||||
|
@ -195,6 +196,7 @@ 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.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}
|
||||||
|
@ -247,7 +249,7 @@ func (c *Client) retryHTTPBackoff(min, max time.Duration, attemptNum int, resp *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the rate limit backoff function when we are rate limited.
|
// Use the rate limit backoff function when we are rate limited.
|
||||||
if resp.StatusCode == 429 {
|
if resp != nil && resp.StatusCode == 429 {
|
||||||
return rateLimitBackoff(min, max, attemptNum, resp)
|
return rateLimitBackoff(min, max, attemptNum, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -290,7 +290,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.14
|
# github.com/hashicorp/go-tfe v0.3.16
|
||||||
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