Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ require (
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-tfe/v2 v2.6.0
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/serf v0.10.2 // indirect
Expand Down Expand Up @@ -228,6 +229,12 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mergestat/timediff v0.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microsoft/kiota-abstractions-go v1.9.4 // indirect
github.com/microsoft/kiota-http-go v1.5.6 // indirect
github.com/microsoft/kiota-serialization-form-go v1.1.3 // indirect
github.com/microsoft/kiota-serialization-json-go v1.1.2 // indirect
github.com/microsoft/kiota-serialization-multipart-go v1.1.2 // indirect
github.com/microsoft/kiota-serialization-text-go v1.1.3 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/iochan v1.0.0 // indirect
Expand Down Expand Up @@ -255,6 +262,7 @@ require (
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.10 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 // indirect
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2w
github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
github.com/hashicorp/go-tfe v1.110.0 h1:R61zw8hgXH+A06rb77GhZXkexjiTls/sPM+lL3G4VsE=
github.com/hashicorp/go-tfe v1.110.0/go.mod h1:VH4URSfSw6421VEBdfjub/oTINTvT5Mhp4Gd9IA3Ifw=
github.com/hashicorp/go-tfe/v2 v2.6.0 h1:1CItfvWIAE09qLr644qDkzl0KZBv08O9Gu7je8CLSK4=
github.com/hashicorp/go-tfe/v2 v2.6.0/go.mod h1:gosuJ9PH3NLxkCoCW3EIeHHli+5QqLUkboBiUZ1ljCM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
Expand Down Expand Up @@ -513,6 +515,18 @@ github.com/mergestat/timediff v0.0.4/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OX
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microsoft/kiota-abstractions-go v1.9.4 h1:VI3UVzSCQHHhRswe3jyaAQHUQWIFhUMp0z5mtZbTbcs=
github.com/microsoft/kiota-abstractions-go v1.9.4/go.mod h1:f06pl3qSyvUHEfVNkiRpXPkafx7khZqQEb71hN/pmuU=
github.com/microsoft/kiota-http-go v1.5.6 h1:KBdk7sxWYXZnRRExLjIcNt4I7LoOfh/XQJWWid4zBKE=
github.com/microsoft/kiota-http-go v1.5.6/go.mod h1:bpJkXfBAcnmiXRg03GXdnb/vF3Sqk3+EgLvXXjmzzQM=
github.com/microsoft/kiota-serialization-form-go v1.1.3 h1:eUY8eHXPFe4ma8cAdx0ya3g4NPlZgbPT+GlFC3xcgGY=
github.com/microsoft/kiota-serialization-form-go v1.1.3/go.mod h1:RMO99zyik+NvZjdVcIeyu6ikyfuKhQtzq2RK0fWJJio=
github.com/microsoft/kiota-serialization-json-go v1.1.2 h1:eJrPWeQ665nbjO0gsHWJ0Bw6V/ZHHU1OfFPaYfRG39k=
github.com/microsoft/kiota-serialization-json-go v1.1.2/go.mod h1:deaGt7fjZarywyp7TOTiRsjfYiyWxwJJPQZytXwYQn8=
github.com/microsoft/kiota-serialization-multipart-go v1.1.2 h1:1pUyA1QgIeKslQwbk7/ox1TehjlCUUT3r1f8cNlkvn4=
github.com/microsoft/kiota-serialization-multipart-go v1.1.2/go.mod h1:j2K7ZyYErloDu7Kuuk993DsvfoP7LPWvAo7rfDpdPio=
github.com/microsoft/kiota-serialization-text-go v1.1.3 h1:8z7Cebn0YAAr++xswVgfdxZjnAZ4GOB9O7XP4+r5r/M=
github.com/microsoft/kiota-serialization-text-go v1.1.3/go.mod h1:NDSvz4A3QalGMjNboKKQI9wR+8k+ih8UuagNmzIRgTQ=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
Expand Down Expand Up @@ -635,6 +649,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.10 h1:8sNWJ4i8eORl8gu97a8EcpRT/y0oapwjddxrf/fbj7w=
github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.10/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
Expand Down
50 changes: 50 additions & 0 deletions internal/cloud/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/hashicorp/cli"
tfe "github.com/hashicorp/go-tfe"
tfev2 "github.com/hashicorp/go-tfe/v2"
version "github.com/hashicorp/go-version"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/disco"
Expand Down Expand Up @@ -66,6 +67,11 @@ type Cloud struct {
// client is the HCP Terraform or Terraform Enterprise API client.
client *tfe.Client

// clientV2 is the go-tfe v2 (Kiota-generated) client, used when features
// such as PolicyPaths require fields not yet in the v1 SDK. It is nil when
// the v2 client cannot be initialised; callers must check before use.
clientV2 *tfev2.Client

// viewHooks implements functions integrating the tfe.Client with the CLI
// output.
viewHooks views.CloudHooks
Expand Down Expand Up @@ -355,6 +361,30 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics {
))
return diags
}

// Initialise the v2 Kiota client. Failure is non-fatal; features that
// need it will fall back to the v1 path when clientV2 is nil.
//
// Split the service URL into scheme+host and path so that Enterprise
// servers with a non-standard base path (e.g. /tfe/api/v2/) are
// routed correctly. Passing the full URL as Address causes tfev2 to
// overwrite the path with its default /api/v2, losing the prefix.
//
// The Kiota URL template uses {+baseurl}/resource, so the base path
// must not have a trailing slash or the request URL will gain a double
// slash (e.g. /tfe/api/v2//queries). Strip it here.
v2Headers := cfg.Headers.Clone()
v2cfg := &tfev2.Config{
Address: tfcService.Scheme + "://" + tfcService.Host,
BasePath: strings.TrimRight(tfcService.Path, "/"),
Token: token,
Headers: v2Headers,
}
if v2client, v2err := tfev2.NewClient(v2cfg); v2err == nil {
b.clientV2 = v2client
} else {
log.Printf("[WARN] cloud: failed to create go-tfe v2 client: %s", v2err)
}
}

// Read the app name header and if empty, provide a default
Expand Down Expand Up @@ -901,6 +931,26 @@ func (b *Cloud) Operation(ctx context.Context, op *backendrun.Operation) (*backe
// Record that we're forced to run operations locally to allow the
// command package UI to operate correctly
b.forceLocal = true

// When the workspace is in local execution mode (not forced via
// TF_FORCE_LOCAL_BACKEND) and the caller supplied --policies paths but
// no policy client was started (because IsLocalOperations() returned
// false before Operation() was called), warn the user that policy
// evaluation will be skipped.
if isLocalExecutionMode(w.ExecutionMode) && len(op.PolicyPaths) > 0 && op.PolicyClient == nil {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Policy evaluation skipped",
"The workspace is configured for local execution mode but the local "+
"policy engine could not be initialised before the operation was "+
"dispatched. Policy paths were provided with -policies but no "+
"policies will be evaluated. Re-run with TF_FORCE_LOCAL_BACKEND=1 "+
"or switch the workspace to remote execution mode.",
))
op.View.Diagnostics(diags)
}

return b.local.Operation(ctx, op)
}

Expand Down
108 changes: 98 additions & 10 deletions internal/cloud/backend_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
"time"

tfe "github.com/hashicorp/go-tfe"
tfev2models "github.com/hashicorp/go-tfe/v2/api/models"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/command/jsonformat"
"github.com/hashicorp/terraform/internal/command/views"
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/genconfig"
"github.com/hashicorp/terraform/internal/terraform"
Expand Down Expand Up @@ -71,7 +73,12 @@ func (b *Cloud) query(stopCtx, cancelCtx context.Context, op *backendrun.Operati
}
queryRunOptions.Variables = runVariables

r, err := b.client.QueryRuns.Create(stopCtx, queryRunOptions)
var r *tfe.QueryRun
if len(op.PolicyPaths) > 0 && b.clientV2 != nil {
r, err = b.createQueryRunV2(stopCtx, queryRunOptions, op.PolicyPaths)
} else {
r, err = b.client.QueryRuns.Create(stopCtx, queryRunOptions)
}
if err != nil {
return &QueryRunResult{}, b.generalError("Failed to create query run", err)
}
Expand Down Expand Up @@ -124,8 +131,8 @@ func (b *Cloud) renderQueryRunLogs(ctx context.Context, op *backendrun.Operation
}

if next || len(line) > 0 {
log := &jsonformat.JSONLog{}
if err := json.Unmarshal(line, log); err != nil {
jsonLog := &jsonformat.JSONLog{}
if err := json.Unmarshal(line, jsonLog); err != nil {
// If we can not parse the line as JSON, we will simply
// print the line. This maintains backwards compatibility for
// users who do not wish to enable structured output in their
Expand All @@ -139,17 +146,25 @@ func (b *Cloud) renderQueryRunLogs(ctx context.Context, op *backendrun.Operation
// we collect all logs of a list block and output them at once.
// This allows us to ensure all messages of a list block are grouped
// and indented as in the PostListQuery hook.
switch log.Type {
switch jsonLog.Type {
case jsonformat.LogPolicyQuerySummary:
rendered, renderErr := views.RenderPolicyQuerySummaryFromJSON(line)
if renderErr != nil {
// Malformed record — skip gracefully.
log.Printf("[TRACE] cloud: skipping malformed policy_query_summary record: %v", renderErr)
continue
}
b.renderer.Streams.Println(rendered)
case jsonformat.LogListStart:
results[log.ListQueryStart.Address] = make([]*viewsjson.QueryResult, 0)
results[jsonLog.ListQueryStart.Address] = make([]*viewsjson.QueryResult, 0)
case jsonformat.LogListResourceFound:
results[log.ListQueryResult.Address] = append(results[log.ListQueryResult.Address], log.ListQueryResult)
results[jsonLog.ListQueryResult.Address] = append(results[jsonLog.ListQueryResult.Address], jsonLog.ListQueryResult)
if wantConfig {
configs[log.ListQueryResult.Address] +=
fmt.Sprintf("%s\n%s\n\n", log.ListQueryResult.Config, log.ListQueryResult.ImportConfig)
configs[jsonLog.ListQueryResult.Address] +=
fmt.Sprintf("%s\n%s\n\n", jsonLog.ListQueryResult.Config, jsonLog.ListQueryResult.ImportConfig)
}
case jsonformat.LogListComplete:
addr := log.ListQueryComplete.Address
addr := jsonLog.ListQueryComplete.Address

identities := make([]string, 0, len(results[addr]))
displayNames := make([]string, 0, len(results[addr]))
Expand All @@ -173,7 +188,7 @@ func (b *Cloud) renderQueryRunLogs(ctx context.Context, op *backendrun.Operation
b.renderer.Streams.Println(result.String())
}
default:
err := b.renderer.RenderLog(log)
err := b.renderer.RenderLog(jsonLog)
if err != nil {
return err
}
Expand Down Expand Up @@ -206,6 +221,79 @@ func (b *Cloud) renderQueryRunLogs(ctx context.Context, op *backendrun.Operation
return nil
}

// createQueryRunV2 creates a query run via the go-tfe v2 SDK so that
// PolicyPaths can be forwarded to the server. It posts to POST /queries
// using the generated QueriesRequestBuilder and maps the response back into
// the tfe.QueryRun that the rest of the cloud backend uses.
func (b *Cloud) createQueryRunV2(ctx context.Context, opts tfe.QueryRunCreateOptions, policyPaths []string) (*tfe.QueryRun, error) {
workspaceID := opts.Workspace.ID

// Build attributes.
attrs := tfev2models.NewQueries_attributes()
if opts.GenerateConfigOut != nil {
attrs.SetGenerateConfigOut(opts.GenerateConfigOut)
}
sourceVal := tfev2models.TFEAPI_QUERIES_ATTRIBUTES_SOURCE
attrs.SetSource(&sourceVal)
attrs.SetPolicyPaths(policyPaths)

// Configuration-version relationship.
cvData := tfev2models.NewConfigurationVersionsHasOne_data()
cvID := opts.ConfigurationVersion.ID
cvData.SetId(&cvID)
cvRef := tfev2models.NewConfigurationVersionsHasOne()
cvRef.SetData(cvData)

// Workspace relationship.
wsData := tfev2models.NewWorkspacesHasOne_data()
wsData.SetId(&workspaceID)
wsRef := tfev2models.NewWorkspacesHasOne()
wsRef.SetData(wsData)

rels := tfev2models.NewQueries_relationships()
rels.SetConfigurationVersion(cvRef)
rels.SetWorkspace(wsRef)

data := tfev2models.NewQueries()
data.SetAttributes(attrs)
data.SetRelationships(rels)

envelope := tfev2models.NewQueriesEnvelope()
envelope.SetData(data)

resp, err := b.clientV2.API.Queries().Post(ctx, envelope, nil)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("empty response from query run create")
}

qry := resp.GetData()

// Map the v2 response back into the tfe.QueryRun the rest of the
// cloud backend already knows how to handle.
run := &tfe.QueryRun{}
if qry != nil {
if qry.GetId() != nil {
run.ID = *qry.GetId()
}
if qry.GetAttributes() != nil {
a := qry.GetAttributes()
if a.GetLogReadUrl() != nil {
run.LogReadURL = *a.GetLogReadUrl()
}
if a.GetStatus() != nil {
run.Status = tfe.QueryRunStatus(a.GetStatus().String())
}
}
}
if run.Status == "" {
run.Status = tfe.QueryRunPending
}
return run, nil
}

func (b *Cloud) waitForQueryRun(stopCtx, cancelCtx context.Context, r *tfe.QueryRun) (*tfe.QueryRun, error) {
started := time.Now()
updated := started
Expand Down
Loading