Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle unknown values in ephemeral resource config #35958

Merged
merged 4 commits into from
Nov 7, 2024
Merged
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
7 changes: 7 additions & 0 deletions internal/command/views/hook_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ func (h *jsonHook) PostRefresh(id terraform.HookResourceIdentity, dk addrs.Depos
}

func (h *jsonHook) PreEphemeralOp(id terraform.HookResourceIdentity, action plans.Action) (terraform.HookAction, error) {
// this uses the same plans.Read action as a data source to indicate that
// the ephemeral resource can't be processed until apply, so there is no
// progress hook
if action == plans.Read {
return terraform.HookActionContinue, nil
}

h.view.Hook(json.NewEphemeralOpStart(id.Addr, action))
progress := resourceProgress{
addr: id.Addr,
Expand Down
20 changes: 15 additions & 5 deletions internal/command/views/hook_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ func (h *UiHook) PreEphemeralOp(rId terraform.HookResourceIdentity, action plans
var operation string
var op uiResourceOp
switch action {
case plans.Read:
// FIXME: this uses the same semantics as data sources, where "read"
// means deferred until apply, but because data sources don't implement
// hooks, and the meaning of Read is overloaded, we can't rely on any
// existing hooks
operation = "Configuration unknown, deferring..."
case plans.Open:
operation = "Opening..."
op = uiResourceOpen
Expand All @@ -367,6 +373,15 @@ func (h *UiHook) PreEphemeralOp(rId terraform.HookResourceIdentity, action plans
return terraform.HookActionContinue, nil
}

h.println(fmt.Sprintf(
h.view.colorize.Color("[reset][bold]%s: %s"),
rId.Addr, operation,
))

if action == plans.Read {
return terraform.HookActionContinue, nil
}

uiState := uiResourceState{
Address: key,
Op: op,
Expand All @@ -379,11 +394,6 @@ func (h *UiHook) PreEphemeralOp(rId terraform.HookResourceIdentity, action plans
h.resources[key] = uiState
h.resourcesLock.Unlock()

h.println(fmt.Sprintf(
h.view.colorize.Color("[reset][bold]%s: %s"),
rId.Addr, operation,
))

go h.stillRunning(uiState)

return terraform.HookActionContinue, nil
Expand Down
5 changes: 5 additions & 0 deletions internal/resources/ephemeral/ephemeral_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ type resourceInstanceInternal struct {
func (r *resourceInstanceInternal) close(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics

// if the resource could not be opened, there will not be anything to close either
if r.impl == nil {
return diags
}

// Stop renewing, if indeed we are. If we previously saw any errors during
// renewing then they finally get returned here, to be reported along with
// any errors during close.
Expand Down
81 changes: 81 additions & 0 deletions internal/terraform/context_plan_ephemeral_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,3 +652,84 @@ ephemeral "ephem_resource" "data" {
})
}
}

func TestContext2Apply_ephemeralUnknownPlan(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_instance" "test" {
}

ephemeral "ephem_resource" "data" {
input = test_instance.test.id
lifecycle {
postcondition {
condition = self.value != nil
error_message = "should return a value"
}
}
}

locals {
value = ephemeral.ephem_resource.data.value
}

// create a sink for the ephemeral value to test
provider "sink" {
test_string = local.value
}

// we need a resource to ensure the sink provider is configured
resource "sink_object" "empty" {
}
`,
})

ephem := &testing_provider.MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
EphemeralResourceTypes: map[string]providers.Schema{
"ephem_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Computed: true,
},
"input": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
}

sink := simpleMockProvider()
sink.GetProviderSchemaResponse.ResourceTypes = map[string]providers.Schema{
"sink_object": {Block: simpleTestSchema()},
}
sink.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
if req.Config.GetAttr("test_string").IsKnown() {
t.Error("sink provider config should not be known in this test")
}
return resp
}

p := testProvider("test")

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem),
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
addrs.NewDefaultProvider("sink"): testProviderFuncFixed(sink),
},
})

_, diags := ctx.Plan(m, nil, DefaultPlanOpts)
assertNoDiagnostics(t, diags)

if ephem.OpenEphemeralResourceCalled {
t.Error("OpenEphemeralResourceCalled called when config was not known")
}
}
38 changes: 33 additions & 5 deletions internal/terraform/node_resource_ephemeral.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid
return nil, diags
}

rId := HookResourceIdentity{
Addr: inp.addr,
ProviderAddr: inp.providerConfig.Provider,
}

ephemerals := ctx.EphemeralResources()
allInsts := ctx.InstanceExpander()
keyData := allInsts.GetResourceInstanceRepetitionData(inp.addr)
Expand All @@ -73,6 +78,34 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid
}
unmarkedConfigVal, configMarks := configVal.UnmarkDeepWithPaths()

if !unmarkedConfigVal.IsWhollyKnown() {
log.Printf("[DEBUG] ehpemeralResourceOpen: configuration for %s contains unknown values, cannot open resource", inp.addr)

// We don't know what the result will be, but we need to keep the
// configured attributes for consistent evaluation. We can use the same
// technique we used for data sources to create the plan-time value.
unknownResult := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal)
// add back any configured marks
unknownResult = unknownResult.MarkWithPaths(configMarks)
// and mark the entire value as ephemeral, since it's coming from an ephemeral context.
unknownResult = unknownResult.Mark(marks.Ephemeral)

// The state of ephemerals all comes from the registered instances, so
// we still need to register something so evaluation doesn't fail.
ephemerals.RegisterInstance(ctx.StopCtx(), inp.addr, ephemeral.ResourceInstanceRegistration{
Value: unknownResult,
ConfigBody: config.Config,
})

ctx.Hook(func(h Hook) (HookAction, error) {
// ephemeral resources aren't stored in the plan, so use a hook to
// give some feedback to the user that this can't be opened
return h.PreEphemeralOp(rId, plans.Read)
})

return nil, diags
}

validateResp := provider.ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest{
TypeName: inp.addr.Resource.Resource.Type,
Config: unmarkedConfigVal,
Expand All @@ -83,11 +116,6 @@ func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*provid
return nil, diags
}

rId := HookResourceIdentity{
Addr: inp.addr,
ProviderAddr: inp.providerConfig.Provider,
}

ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreEphemeralOp(rId, plans.Open)
})
Expand Down
Loading