Merge pull request #35958 from hashicorp/jbardin/uknown-ephemeral-config

Handle unknown values in ephemeral resource config
pull/37026/head
James Bardin 1 year ago committed by GitHub
commit 896f9e74b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

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

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

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

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

Loading…
Cancel
Save