Merge pull request #21102 from hashicorp/pault/remote-backend-go-tfe-update

update to latest go-tfe
This commit is contained in:
Paul Thrasher 2019-04-26 10:05:00 -07:00 committed by GitHub
commit 6183ca44c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 365 additions and 22 deletions

View File

@ -227,6 +227,57 @@ func (b *Remote) parseVariableValues(op *backend.Operation) (terraform.InputValu
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 {
if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------\n")

View File

@ -21,6 +21,7 @@ import (
type mockClient struct {
Applies *mockApplies
ConfigurationVersions *mockConfigurationVersions
CostEstimations *mockCostEstimations
Organizations *mockOrganizations
Plans *mockPlans
PolicyChecks *mockPolicyChecks
@ -33,6 +34,7 @@ func newMockClient() *mockClient {
c := &mockClient{}
c.Applies = newMockApplies(c)
c.ConfigurationVersions = newMockConfigurationVersions(c)
c.CostEstimations = newMockCostEstimations(c)
c.Organizations = newMockOrganizations(c)
c.Plans = newMockPlans(c)
c.PolicyChecks = newMockPolicyChecks(c)
@ -212,6 +214,84 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string
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.
type mockInput struct {
answers map[string]string
@ -652,19 +732,25 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t
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)
if err != nil {
return nil, err
}
r := &tfe.Run{
ID: generateID("run-"),
Actions: &tfe.RunActions{IsCancelable: true},
Apply: a,
HasChanges: false,
Permissions: &tfe.RunPermissions{},
Plan: p,
Status: tfe.RunPending,
ID: generateID("run-"),
Actions: &tfe.RunActions{IsCancelable: true},
Apply: a,
CostEstimation: ce,
HasChanges: false,
Permissions: &tfe.RunPermissions{},
Plan: p,
Status: tfe.RunPending,
}
if pc != nil {

View File

@ -290,6 +290,14 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation,
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.
if len(r.PolicyChecks) > 0 {
err = b.checkPolicy(stopCtx, cancelCtx, op, r)

View File

@ -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) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
@ -681,12 +715,12 @@ func TestRemote_planPolicyPass(t *testing.T) {
if !strings.Contains(output, "Running plan in the remote backend") {
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") {
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) {
@ -720,12 +754,12 @@ func TestRemote_planPolicyHardFail(t *testing.T) {
if !strings.Contains(output, "Running plan in the remote backend") {
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") {
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) {
@ -759,12 +793,12 @@ func TestRemote_planPolicySoftFail(t *testing.T) {
if !strings.Contains(output, "Running plan in the remote backend") {
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") {
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) {

View File

@ -0,0 +1,6 @@
+---------+------+-----+-------------+----------------------+
| PRODUCT | NAME | SKU | DESCRIPTION | DELTA |
+---------+------+-----+-------------+----------------------+
+---------+------+-----+-------------+----------------------+
| TOTAL | $0.000 USD / 720 HRS |
+---------+------+-----+-------------+----------------------+

View File

@ -0,0 +1 @@
resource "null_resource" "foo" {}

View File

@ -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.

View File

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

2
go.mod
View File

@ -55,7 +55,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.5.2
github.com/hashicorp/go-rootcerts v1.0.0
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
github.com/hashicorp/go-tfe v0.3.14
github.com/hashicorp/go-tfe v0.3.16
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/go-version v1.1.0
github.com/hashicorp/golang-lru v0.5.0 // indirect

4
go.sum
View File

@ -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-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-tfe v0.3.14 h1:1eWmq4RAICGufydNUWu7ahb0gtq24pN9jatD2FkdxdE=
github.com/hashicorp/go-tfe v0.3.14/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
github.com/hashicorp/go-tfe v0.3.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0=
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
github.com/hashicorp/go-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.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=

121
vendor/github.com/hashicorp/go-tfe/cost_estimation.go generated vendored Normal file
View File

@ -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
}
}

View File

@ -77,6 +77,7 @@ type OrganizationList struct {
type Organization struct {
Name string `jsonapi:"primary,organizations"`
CollaboratorAuthPolicy AuthPolicyType `jsonapi:"attr,collaborator-auth-policy"`
CostEstimationEnabled bool `jsonapi:"attr,cost-estimation-enabled"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
Email string `jsonapi:"attr,email"`
EnterprisePlan EnterprisePlanType `jsonapi:"attr,enterprise-plan"`
@ -167,6 +168,9 @@ type OrganizationCreateOptions struct {
// Authentication policy.
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
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
}
@ -248,6 +252,9 @@ type OrganizationUpdateOptions struct {
// Authentication policy.
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
OwnersTeamSAMLRoleID *string `jsonapi:"attr,owners-team-saml-role-id,omitempty"`
}

View File

@ -49,12 +49,16 @@ type RunStatus string
//List all available run statuses.
const (
RunApplied RunStatus = "applied"
RunApplyQueued RunStatus = "apply_queued"
RunApplying RunStatus = "applying"
RunCanceled RunStatus = "canceled"
RunConfirmed RunStatus = "confirmed"
RunCostEstimated RunStatus = "cost_estimated"
RunCostEstimating RunStatus = "cost_estimating"
RunDiscarded RunStatus = "discarded"
RunErrored RunStatus = "errored"
RunPending RunStatus = "pending"
RunPlanQueued RunStatus = "plan_queued"
RunPlanned RunStatus = "planned"
RunPlannedAndFinished RunStatus = "planned_and_finished"
RunPlanning RunStatus = "planning"
@ -98,6 +102,7 @@ type Run struct {
// Relations
Apply *Apply `jsonapi:"relation,apply"`
ConfigurationVersion *ConfigurationVersion `jsonapi:"relation,configuration-version"`
CostEstimation *CostEstimation `jsonapi:"relation,cost-estimation"`
Plan *Plan `jsonapi:"relation,plan"`
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
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)
}
// RunCancelOptions represents the options for force-canceling a run.
// RunForceCancelOptions represents the options for force-canceling a run.
type RunForceCancelOptions struct {
// An optional comment explaining the reason for the force-cancel.
Comment *string `json:"comment,omitempty"`

View File

@ -106,6 +106,7 @@ type Client struct {
Applies Applies
ConfigurationVersions ConfigurationVersions
CostEstimations CostEstimations
NotificationConfigurations NotificationConfigurations
OAuthClients OAuthClients
OAuthTokens OAuthTokens
@ -195,6 +196,7 @@ func NewClient(cfg *Config) (*Client, error) {
// Create the services.
client.Applies = &applies{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
client.CostEstimations = &costEstimations{client: client}
client.NotificationConfigurations = &notificationConfigurations{client: client}
client.OAuthClients = &oAuthClients{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.
if resp.StatusCode == 429 {
if resp != nil && resp.StatusCode == 429 {
return rateLimitBackoff(min, max, attemptNum, resp)
}

2
vendor/modules.txt vendored
View File

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