Merge pull request #26495 from hashicorp/jbardin/evaluate-walks

Audit some graph building and GraphNodeExecutable
This commit is contained in:
James Bardin 2020-10-07 09:20:05 -04:00 committed by GitHub
commit 496cf629cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 232 additions and 282 deletions

View File

@ -38,7 +38,7 @@ func TestContextEval(t *testing.T) {
}, },
{ {
`module.child.result`, `module.child.result`,
cty.UnknownVal(cty.Number), cty.NumberIntVal(6),
false, false,
}, },
} }

View File

@ -87,6 +87,12 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
Config: b.Config, Config: b.Config,
}, },
// Add dynamic values
&RootVariableTransformer{Config: b.Config},
&ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config},
// Creates all the resource instances represented in the diff, along // Creates all the resource instances represented in the diff, along
// with dependency edges against the whole-resource nodes added by // with dependency edges against the whole-resource nodes added by
// ConfigTransformer above. // ConfigTransformer above.
@ -96,31 +102,19 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
Changes: b.Changes, Changes: b.Changes,
}, },
// Attach the state
&AttachStateTransformer{State: b.State},
// Create orphan output nodes // Create orphan output nodes
&OrphanOutputTransformer{Config: b.Config, State: b.State}, &OrphanOutputTransformer{Config: b.Config, State: b.State},
// Attach the configuration to any resources // Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config}, &AttachResourceConfigTransformer{Config: b.Config},
// Attach the state
&AttachStateTransformer{State: b.State},
// Provisioner-related transformations // Provisioner-related transformations
&MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()}, &MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()},
&ProvisionerTransformer{}, &ProvisionerTransformer{},
// Add root variables
&RootVariableTransformer{Config: b.Config},
// Add the local values
&LocalTransformer{Config: b.Config},
// Add the outputs
&OutputTransformer{Config: b.Config},
// Add module variables
&ModuleVariableTransformer{Config: b.Config},
// add providers // add providers
TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config), TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config),
@ -150,7 +144,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
State: b.State, State: b.State,
Schemas: b.Schemas, Schemas: b.Schemas,
}, },
&CBDEdgeTransformer{ &CBDEdgeTransformer{
Config: b.Config, Config: b.Config,
State: b.State, State: b.State,

View File

@ -92,8 +92,13 @@ func (b *DestroyPlanGraphBuilder) Steps() []GraphTransformer {
// created proper destroy ordering. // created proper destroy ordering.
&TargetsTransformer{Targets: b.Targets}, &TargetsTransformer{Targets: b.Targets},
// Close opened plugin connections
&CloseProviderTransformer{},
// Close the root module // Close the root module
&CloseRootModuleTransformer{}, &CloseRootModuleTransformer{},
&TransitiveReductionTransformer{},
} }
return steps return steps

View File

@ -60,28 +60,20 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
// Creates all the data resources that aren't in the state. This will also // Creates all the data resources that aren't in the state. This will also
// add any orphans from scaling in as destroy nodes. // add any orphans from scaling in as destroy nodes.
&ConfigTransformer{ &ConfigTransformer{
Concrete: nil, // just use the abstract type Config: b.Config,
Config: b.Config,
Unique: true,
}, },
// Attach the state // Add dynamic values
&AttachStateTransformer{State: b.State}, &RootVariableTransformer{Config: b.Config},
&ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config},
// Attach the configuration to any resources // Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config}, &AttachResourceConfigTransformer{Config: b.Config},
// Add root variables // Attach the state
&RootVariableTransformer{Config: b.Config}, &AttachStateTransformer{State: b.State},
// Add the local values
&LocalTransformer{Config: b.Config},
// Add the outputs
&OutputTransformer{Config: b.Config},
// Add module variables
&ModuleVariableTransformer{Config: b.Config},
TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config), TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config),
@ -92,9 +84,7 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
// Create expansion nodes for all of the module calls. This must // Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing // come after all other transformers that create nodes representing
// objects that can belong to modules. // objects that can belong to modules.
&ModuleExpansionTransformer{ &ModuleExpansionTransformer{Config: b.Config},
Config: b.Config,
},
// Connect so that the references are ready for targeting. We'll // Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on. // have to connect again later for providers and so on.

View File

@ -55,26 +55,20 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
// Create all our resources from the configuration and state // Create all our resources from the configuration and state
&ConfigTransformer{Config: config}, &ConfigTransformer{Config: config},
// Add dynamic values
&RootVariableTransformer{Config: b.Config},
&ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config},
// Attach the configuration to any resources // Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config}, &AttachResourceConfigTransformer{Config: b.Config},
// Add the import steps // Add the import steps
&ImportStateTransformer{Targets: b.ImportTargets, Config: b.Config}, &ImportStateTransformer{Targets: b.ImportTargets, Config: b.Config},
// Add root variables
&RootVariableTransformer{Config: b.Config},
TransformProviders(b.Components.ResourceProviders(), concreteProvider, config), TransformProviders(b.Components.ResourceProviders(), concreteProvider, config),
// Add the local values
&LocalTransformer{Config: b.Config},
// Add the outputs
&OutputTransformer{Config: b.Config},
// Add module variables
&ModuleVariableTransformer{Config: b.Config},
// Must attach schemas before ReferenceTransformer so that we can // Must attach schemas before ReferenceTransformer so that we can
// analyze the configuration to find references. // analyze the configuration to find references.
&AttachSchemaTransformer{Schemas: b.Schemas, Config: b.Config}, &AttachSchemaTransformer{Schemas: b.Schemas, Config: b.Config},
@ -82,9 +76,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
// Create expansion nodes for all of the module calls. This must // Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing // come after all other transformers that create nodes representing
// objects that can belong to modules. // objects that can belong to modules.
&ModuleExpansionTransformer{ &ModuleExpansionTransformer{Config: b.Config},
Config: b.Config,
},
// Connect so that the references are ready for targeting. We'll // Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on. // have to connect again later for providers and so on.

View File

@ -84,10 +84,10 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
Config: b.Config, Config: b.Config,
}, },
// Add the local values // Add dynamic values
&RootVariableTransformer{Config: b.Config},
&ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config}, &LocalTransformer{Config: b.Config},
// Add the outputs
&OutputTransformer{Config: b.Config}, &OutputTransformer{Config: b.Config},
// Add orphan resources // Add orphan resources
@ -106,29 +106,20 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
State: b.State, State: b.State,
}, },
// Attach the state
&AttachStateTransformer{State: b.State},
// Create orphan output nodes // Create orphan output nodes
&OrphanOutputTransformer{ &OrphanOutputTransformer{Config: b.Config, State: b.State},
Config: b.Config,
State: b.State,
},
// Attach the configuration to any resources // Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config}, &AttachResourceConfigTransformer{Config: b.Config},
// Attach the state // Provisioner-related transformations
&AttachStateTransformer{State: b.State},
// Add root variables
&RootVariableTransformer{Config: b.Config},
&MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()}, &MissingProvisionerTransformer{Provisioners: b.Components.ResourceProvisioners()},
&ProvisionerTransformer{}, &ProvisionerTransformer{},
// Add module variables // add providers
&ModuleVariableTransformer{
Config: b.Config,
},
TransformProviders(b.Components.ResourceProviders(), b.ConcreteProvider, b.Config), TransformProviders(b.Components.ResourceProviders(), b.ConcreteProvider, b.Config),
// Remove modules no longer present in the config // Remove modules no longer present in the config
@ -141,10 +132,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// Create expansion nodes for all of the module calls. This must // Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing // come after all other transformers that create nodes representing
// objects that can belong to modules. // objects that can belong to modules.
&ModuleExpansionTransformer{ &ModuleExpansionTransformer{Concrete: b.ConcreteModule, Config: b.Config},
Concrete: b.ConcreteModule,
Config: b.Config,
},
// Connect so that the references are ready for targeting. We'll // Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on. // have to connect again later for providers and so on.
@ -156,9 +144,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
&attachDataResourceDependenciesTransformer{}, &attachDataResourceDependenciesTransformer{},
// Target // Target
&TargetsTransformer{ &TargetsTransformer{Targets: b.Targets},
Targets: b.Targets,
},
// Detect when create_before_destroy must be forced on for a particular // Detect when create_before_destroy must be forced on for a particular
// node due to dependency edges, to avoid graph cycles during apply. // node due to dependency edges, to avoid graph cycles during apply.
@ -171,7 +157,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// Close opened plugin connections // Close opened plugin connections
&CloseProviderTransformer{}, &CloseProviderTransformer{},
&CloseProvisionerTransformer{},
// Close the root module // Close the root module
&CloseRootModuleTransformer{}, &CloseRootModuleTransformer{},

View File

@ -153,13 +153,13 @@ func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) error {
var err error var err error
switch op { switch op {
case walkPlan, walkApply, walkDestroy: case walkValidate:
vals, err = n.EvalModuleCallArgument(ctx, false) vals, err = n.EvalModuleCallArgument(ctx, true)
if err != nil { if err != nil {
return err return err
} }
case walkValidate: default:
vals, err = n.EvalModuleCallArgument(ctx, true) vals, err = n.EvalModuleCallArgument(ctx, false)
if err != nil { if err != nil {
return err return err
} }

View File

@ -199,61 +199,55 @@ func (n *NodeApplyableOutput) References() []*addrs.Reference {
// GraphNodeExecutable // GraphNodeExecutable
func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) error { func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) error {
switch op { // This has to run before we have a state lock, since evaluation also
// Everything except walkImport // reads the state
case walkEval, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy: val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
// This has to run before we have a state lock, since evaluation also // We'll handle errors below, after we have loaded the module.
// reads the state
val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
// We'll handle errors below, after we have loaded the module.
// Outputs don't have a separate mode for validation, so validate // Outputs don't have a separate mode for validation, so validate
// depends_on expressions here too // depends_on expressions here too
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
// Ensure that non-sensitive outputs don't include sensitive values // Ensure that non-sensitive outputs don't include sensitive values
_, marks := val.UnmarkDeep() _, marks := val.UnmarkDeep()
_, hasSensitive := marks["sensitive"] _, hasSensitive := marks["sensitive"]
if !n.Config.Sensitive && hasSensitive { if !n.Config.Sensitive && hasSensitive {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Output refers to sensitive values", Summary: "Output refers to sensitive values",
Detail: "Expressions used in outputs can only refer to sensitive values if the sensitive attribute is true.", Detail: "Expressions used in outputs can only refer to sensitive values if the sensitive attribute is true.",
Subject: n.Config.DeclRange.Ptr(), Subject: n.Config.DeclRange.Ptr(),
}) })
} }
state := ctx.State() state := ctx.State()
if state == nil { if state == nil {
return nil
}
changes := ctx.Changes() // may be nil, if we're not working on a changeset
// handling the interpolation error
if diags.HasErrors() {
if flagWarnOutputErrors {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr, diags.Err())
// if we're continuing, make sure the output is included, and
// marked as unknown. If the evaluator was able to find a type
// for the value in spite of the error then we'll use it.
n.setValue(state, changes, cty.UnknownVal(val.Type()))
return EvalEarlyExitError{}
}
return diags.Err()
}
n.setValue(state, changes, val)
// If we were able to evaluate a new value, we can update that in the
// refreshed state as well.
if state = ctx.RefreshState(); state != nil && val.IsWhollyKnown() {
n.setValue(state, changes, val)
}
return nil
default:
return nil return nil
} }
changes := ctx.Changes() // may be nil, if we're not working on a changeset
// handling the interpolation error
if diags.HasErrors() {
if flagWarnOutputErrors {
log.Printf("[ERROR] Output interpolation %q failed: %s", n.Addr, diags.Err())
// if we're continuing, make sure the output is included, and
// marked as unknown. If the evaluator was able to find a type
// for the value in spite of the error then we'll use it.
n.setValue(state, changes, cty.UnknownVal(val.Type()))
return EvalEarlyExitError{}
}
return diags.Err()
}
n.setValue(state, changes, val)
// If we were able to evaluate a new value, we can update that in the
// refreshed state as well.
if state = ctx.RefreshState(); state != nil && val.IsWhollyKnown() {
n.setValue(state, changes, val)
}
return nil
} }
// dag.GraphNodeDotter impl. // dag.GraphNodeDotter impl.

View File

@ -136,133 +136,130 @@ func (n *NodeDestroyResourceInstance) Execute(ctx EvalContext, op walkOperation)
var state *states.ResourceInstanceObject var state *states.ResourceInstanceObject
var provisionerErr error var provisionerErr error
switch op { provider, providerSchema, err := GetProvider(ctx, n.ResolvedProvider)
case walkApply, walkDestroy: if err != nil {
provider, providerSchema, err := GetProvider(ctx, n.ResolvedProvider) return err
}
changeApply, err = n.readDiff(ctx, providerSchema)
if err != nil {
return err
}
evalReduceDiff := &EvalReduceDiff{
Addr: addr.Resource,
InChange: &changeApply,
Destroy: true,
OutChange: &changeApply,
}
_, err = evalReduceDiff.Eval(ctx)
if err != nil {
return err
}
// EvalReduceDiff may have simplified our planned change
// into a NoOp if it does not require destroying.
if changeApply == nil || changeApply.Action == plans.NoOp {
return EvalEarlyExitError{}
}
state, err = n.ReadResourceInstanceState(ctx, addr)
if err != nil {
return err
}
// Exit early if the state object is null after reading the state
if state == nil || state.Value.IsNull() {
return EvalEarlyExitError{}
}
evalApplyPre := &EvalApplyPre{
Addr: addr.Resource,
State: &state,
Change: &changeApply,
}
_, err = evalApplyPre.Eval(ctx)
if err != nil {
return err
}
// Run destroy provisioners if not tainted
if state != nil && state.Status != states.ObjectTainted {
evalApplyProvisioners := &EvalApplyProvisioners{
Addr: addr.Resource,
State: &state,
ResourceConfig: n.Config,
Error: &provisionerErr,
When: configs.ProvisionerWhenDestroy,
}
_, err := evalApplyProvisioners.Eval(ctx)
if err != nil { if err != nil {
return err return err
} }
if provisionerErr != nil {
changeApply, err = n.readDiff(ctx, providerSchema) // If we have a provisioning error, then we just call
if err != nil { // the post-apply hook now.
return err evalApplyPost := &EvalApplyPost{
} Addr: addr.Resource,
State: &state,
evalReduceDiff := &EvalReduceDiff{ Error: &provisionerErr,
Addr: addr.Resource,
InChange: &changeApply,
Destroy: true,
OutChange: &changeApply,
}
_, err = evalReduceDiff.Eval(ctx)
if err != nil {
return err
}
// EvalReduceDiff may have simplified our planned change
// into a NoOp if it does not require destroying.
if changeApply == nil || changeApply.Action == plans.NoOp {
return EvalEarlyExitError{}
}
state, err = n.ReadResourceInstanceState(ctx, addr)
if err != nil {
return err
}
// Exit early if the state object is null after reading the state
if state == nil || state.Value.IsNull() {
return EvalEarlyExitError{}
}
evalApplyPre := &EvalApplyPre{
Addr: addr.Resource,
State: &state,
Change: &changeApply,
}
_, err = evalApplyPre.Eval(ctx)
if err != nil {
return err
}
// Run destroy provisioners if not tainted
if state != nil && state.Status != states.ObjectTainted {
evalApplyProvisioners := &EvalApplyProvisioners{
Addr: addr.Resource,
State: &state,
ResourceConfig: n.Config,
Error: &provisionerErr,
When: configs.ProvisionerWhenDestroy,
} }
_, err := evalApplyProvisioners.Eval(ctx) _, err = evalApplyPost.Eval(ctx)
if err != nil { if err != nil {
return err return err
} }
if provisionerErr != nil {
// If we have a provisioning error, then we just call
// the post-apply hook now.
evalApplyPost := &EvalApplyPost{
Addr: addr.Resource,
State: &state,
Error: &provisionerErr,
}
_, err = evalApplyPost.Eval(ctx)
if err != nil {
return err
}
}
} }
}
// Managed resources need to be destroyed, while data sources // Managed resources need to be destroyed, while data sources
// are only removed from state. // are only removed from state.
if addr.Resource.Resource.Mode == addrs.ManagedResourceMode { if addr.Resource.Resource.Mode == addrs.ManagedResourceMode {
evalApply := &EvalApply{ evalApply := &EvalApply{
Addr: addr.Resource, Addr: addr.Resource,
Config: nil, // No configuration because we are destroying Config: nil, // No configuration because we are destroying
State: &state, State: &state,
Change: &changeApply, Change: &changeApply,
Provider: &provider, Provider: &provider,
ProviderAddr: n.ResolvedProvider, ProviderAddr: n.ResolvedProvider,
ProviderMetas: n.ProviderMetas, ProviderMetas: n.ProviderMetas,
ProviderSchema: &providerSchema, ProviderSchema: &providerSchema,
Output: &state, Output: &state,
Error: &provisionerErr, Error: &provisionerErr,
}
_, err = evalApply.Eval(ctx)
if err != nil {
return err
}
evalWriteState := &EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
}
_, err = evalWriteState.Eval(ctx)
if err != nil {
return err
}
} else {
log.Printf("[TRACE] NodeDestroyResourceInstance: removing state object for %s", n.Addr)
state := ctx.State()
state.SetResourceInstanceCurrent(n.Addr, nil, n.ResolvedProvider)
} }
_, err = evalApply.Eval(ctx)
evalApplyPost := &EvalApplyPost{
Addr: addr.Resource,
State: &state,
Error: &provisionerErr,
}
_, err = evalApplyPost.Eval(ctx)
if err != nil { if err != nil {
return err return err
} }
err = UpdateStateHook(ctx) evalWriteState := &EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
}
_, err = evalWriteState.Eval(ctx)
if err != nil { if err != nil {
return err return err
} }
} else {
log.Printf("[TRACE] NodeDestroyResourceInstance: removing state object for %s", n.Addr)
state := ctx.State()
state.SetResourceInstanceCurrent(n.Addr, nil, n.ResolvedProvider)
}
evalApplyPost := &EvalApplyPost{
Addr: addr.Resource,
State: &state,
Error: &provisionerErr,
}
_, err = evalApplyPost.Eval(ctx)
if err != nil {
return err
}
err = UpdateStateHook(ctx)
if err != nil {
return err
} }
return nil return nil

View File

@ -76,44 +76,41 @@ func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx EvalContext, op walk
var change *plans.ResourceInstanceChange var change *plans.ResourceInstanceChange
var state *states.ResourceInstanceObject var state *states.ResourceInstanceObject
switch op { readStateDeposed := &EvalReadStateDeposed{
case walkPlan, walkPlanDestroy: Addr: addr.Resource,
Output: &state,
readStateDeposed := &EvalReadStateDeposed{ Key: n.DeposedKey,
Addr: addr.Resource, Provider: &provider,
Output: &state, ProviderSchema: &providerSchema,
Key: n.DeposedKey,
Provider: &provider,
ProviderSchema: &providerSchema,
}
_, err = readStateDeposed.Eval(ctx)
if err != nil {
return err
}
diffDestroy := &EvalDiffDestroy{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
DeposedKey: n.DeposedKey,
State: &state,
Output: &change,
}
_, err = diffDestroy.Eval(ctx)
if err != nil {
return err
}
writeDiff := &EvalWriteDiff{
Addr: addr.Resource,
DeposedKey: n.DeposedKey,
ProviderSchema: &providerSchema,
Change: &change,
}
_, err = writeDiff.Eval(ctx)
if err != nil {
return err
}
} }
_, err = readStateDeposed.Eval(ctx)
if err != nil {
return err
}
diffDestroy := &EvalDiffDestroy{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
DeposedKey: n.DeposedKey,
State: &state,
Output: &change,
}
_, err = diffDestroy.Eval(ctx)
if err != nil {
return err
}
writeDiff := &EvalWriteDiff{
Addr: addr.Resource,
DeposedKey: n.DeposedKey,
ProviderSchema: &providerSchema,
Change: &change,
}
_, err = writeDiff.Eval(ctx)
if err != nil {
return err
}
return nil return nil
} }

View File

@ -25,9 +25,6 @@ type ConfigTransformer struct {
// Module is the module to add resources from. // Module is the module to add resources from.
Config *configs.Config Config *configs.Config
// Unique will only add resources that aren't already present in the graph.
Unique bool
// Mode will only add resources that match the given mode // Mode will only add resources that match the given mode
ModeFilter bool ModeFilter bool
Mode addrs.ResourceMode Mode addrs.ResourceMode

View File

@ -301,7 +301,7 @@ func (m ReferenceMap) References(v dag.Vertex) []dag.Vertex {
case addrs.ModuleCallInstance: case addrs.ModuleCallInstance:
subject = ri.Call subject = ri.Call
default: default:
log.Printf("[WARN] ReferenceTransformer: reference not found: %q", subject) log.Printf("[INFO] ReferenceTransformer: reference not found: %q", subject)
continue continue
} }
key = m.referenceMapKey(v, subject) key = m.referenceMapKey(v, subject)