diff --git a/go.mod b/go.mod index 2ba4e9d83030..863a8cd4c191 100644 --- a/go.mod +++ b/go.mod @@ -28,13 +28,13 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 github.com/hashicorp/go-retryablehttp v0.7.7 - github.com/hashicorp/go-slug v0.16.3 - github.com/hashicorp/go-tfe v1.74.1 + github.com/hashicorp/go-slug v0.16.4 + github.com/hashicorp/go-tfe v1.88.1-0.20250730143610-3c0fd4f54fc2 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.24.0 - github.com/hashicorp/jsonapi v1.3.2 + github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e github.com/hashicorp/terraform-registry-address v0.3.0 github.com/hashicorp/terraform-svchost v0.1.1 github.com/hashicorp/terraform/internal/backend/remote-state/azure v0.0.0-00010101000000-000000000000 @@ -255,7 +255,7 @@ require ( golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/time v0.10.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.155.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 52722edd336d..fda7a72cac3e 100644 --- a/go.sum +++ b/go.sum @@ -1126,15 +1126,15 @@ github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5O github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= -github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-slug v0.16.4 h1:kI0mOUVjbBsyocwO29pZIQzzkBnfQNdU4eqlUpNdNVA= +github.com/hashicorp/go-slug v0.16.4/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-sockaddr v1.0.5 h1:dvk7TIXCZpmfOlM+9mlcrWmWjw/wlKT+VDq2wMvfPJU= github.com/hashicorp/go-sockaddr v1.0.5/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-tfe v1.74.1 h1:I/8fOwSYox17IZV7SULIQH0ZRPNL2g/biW6hHWnOTVY= -github.com/hashicorp/go-tfe v1.74.1/go.mod h1:kGHWMZ3HHjitgqON8nBZ4kPVJ3cLbzM4JMgmNVMs9aQ= +github.com/hashicorp/go-tfe v1.88.1-0.20250730143610-3c0fd4f54fc2 h1:BdSTYcozPpDhDiovPCSjrdYYvAyDGjlZOHm1fPMQbS4= +github.com/hashicorp/go-tfe v1.88.1-0.20250730143610-3c0fd4f54fc2/go.mod h1:6dUFMBKh0jkxlRsrw7bYD2mby0efdwE4dtlAuTogIzA= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -1152,8 +1152,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= -github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e h1:xwy/1T0cxHWaLx2MM0g4BlaQc1BXn/9835mPrBqwSPU= +github.com/hashicorp/jsonapi v1.4.3-0.20250220162346-81a76b606f3e/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= @@ -1932,8 +1932,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/cloud/backend_plan.go b/internal/cloud/backend_plan.go index 999947ffdc25..d65b2214e435 100644 --- a/internal/cloud/backend_plan.go +++ b/internal/cloud/backend_plan.go @@ -273,6 +273,23 @@ in order to capture the filesystem context the remote workspace expects: } } + if len(op.ActionTargets) != 0 { + if len(op.ActionTargets) > 1 { + // For now, we only support a single action from the command line. + // We've future proofed the API and inputs so we can send multiple + // but versions of Terraform will enforce this both here, and + // on the other side. + // + // It shouldn't actually be possible to reach here anyway - we're + // validating at the point the flag is read that it only has a + // single entry. But, we'll check again to be safe. + return nil, errors.New("unsupported arguments, at most 1 action can be invoked per operation") + } + + // TODO: Switch this over to a slice once go-tfe is updated. + runOptions.InvokeActionAddr = tfe.String(op.ActionTargets[0].String()) + } + if len(op.ForceReplace) != 0 { runOptions.ReplaceAddrs = make([]string, 0, len(op.ForceReplace)) for _, addr := range op.ForceReplace { diff --git a/internal/cloud/backend_plan_test.go b/internal/cloud/backend_plan_test.go index 0bf6e86e746b..7e7fde8bde94 100644 --- a/internal/cloud/backend_plan_test.go +++ b/internal/cloud/backend_plan_test.go @@ -541,6 +541,45 @@ func TestCloud_planWithRefreshOnly(t *testing.T) { } } +func TestCloud_planWithInvoke(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationPlan(t, "./testdata/action") + defer configCleanup() + defer done(t) + + addr, _ := addrs.ParseAbsActionInstanceStr("action.test_action.test") + + op.ActionTargets = append(op.ActionTargets, addr) + op.Workspace = testBackendSingleWorkspaceName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatal("expected plan operation to succeed") + } + if run.PlanEmpty { + t.Fatalf("expected plan to be non-empty") + } + + // We should find a run inside the mock client that has the same + // target address we requested above. + runsAPI := b.client.Runs.(*MockRuns) + if got, want := len(runsAPI.Runs), 1; got != want { + t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) + } + for _, run := range runsAPI.Runs { + if diff := cmp.Diff("action.test_action.test", run.InvokeActionAddr); diff != "" { + t.Errorf("wrong TargetAddrs in the created run\n%s", diff) + } + } +} + func TestCloud_planWithTarget(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() diff --git a/internal/cloud/testdata/action/main.tf b/internal/cloud/testdata/action/main.tf new file mode 100644 index 000000000000..5cb4f85af9f4 --- /dev/null +++ b/internal/cloud/testdata/action/main.tf @@ -0,0 +1,6 @@ + +action "test_action" "action" { + config { + attr = "hello, world" + } +} diff --git a/internal/cloud/testdata/action/plan.log b/internal/cloud/testdata/action/plan.log new file mode 100644 index 000000000000..84ebc5abd998 --- /dev/null +++ b/internal/cloud/testdata/action/plan.log @@ -0,0 +1,22 @@ +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. + +------------------------------------------------------------------------ + +Terraform will invoke the following action(s): + + # action.test_action.test will be invoked + action "test_action" "test" { + config { + program = [ + "curl", + "https://checkpoint-api.hashicorp.com/v1/check/terraform", + ] + } + } + diff --git a/internal/cloud/tfe_client_mock.go b/internal/cloud/tfe_client_mock.go index 5a5ccaf33b70..8268c9caa23a 100644 --- a/internal/cloud/tfe_client_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -1214,6 +1214,13 @@ func (m *MockRegistryModules) ReadVersion(ctx context.Context, moduleID tfe.Regi panic("implement me") } +func (m *MockRegistryModules) ReadTerraformRegistryModule(ctx context.Context, moduleID tfe.RegistryModuleID, version string) (*tfe.TerraformRegistryModule, error) { + //TODO implement me + panic("implement me") +} + +var _ tfe.Runs = (*MockRuns)(nil) + type MockRuns struct { sync.Mutex @@ -1261,6 +1268,11 @@ func (m *MockRuns) List(ctx context.Context, workspaceID string, options *tfe.Ru return rl, nil } +func (m *MockRuns) ListForOrganization(ctx context.Context, organization string, options *tfe.RunListForOrganizationOptions) (*tfe.OrganizationRunList, error) { + //TODO implement me + panic("implement me") +} + func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { m.Lock() defer m.Unlock() @@ -1323,6 +1335,10 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t r.Plan.GeneratedConfiguration = true } + if options.InvokeActionAddr != nil { + r.InvokeActionAddr = *options.InvokeActionAddr + } + w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] if !ok { return nil, tfe.ErrResourceNotFound @@ -1388,7 +1404,8 @@ func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *t hasChanges := r.IsDestroy || bytes.Contains(logs, []byte("1 to add")) || bytes.Contains(logs, []byte("1 to change")) || - bytes.Contains(logs, []byte("1 to import")) + bytes.Contains(logs, []byte("1 to import")) || + bytes.Contains(logs, []byte("Terraform will invoke the following action(s)")) if hasChanges { r.Actions.IsCancelable = false r.Actions.IsConfirmable = true @@ -1887,6 +1904,11 @@ func (m *MockVariables) List(ctx context.Context, workspaceID string, options *t return vl, nil } +func (m *MockVariables) ListAll(ctx context.Context, workspaceID string, options *tfe.VariableListOptions) (*tfe.VariableList, error) { + //TODO implement me + panic("implement me") +} + func (m *MockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { v := &tfe.Variable{ ID: GenerateID("var-"),