-
Notifications
You must be signed in to change notification settings - Fork 352
Description
Summary
CUE fails to properly unify values with disjunction types when the unification occurs inside a for loop or if conditional, resulting in an "incomplete value" error. The same code works correctly when the unification is applied via a pattern constraint after the loop.
CUE Version
cue version (devel)
CUE language version v0.15.0
Expected Behavior
When a value like {Ref: "DBCluster"} is unified with a field constrained to (string) | #Fn where #Fn is a disjunction (#Ref | #Base64), CUE should recognize that the value satisfies the #Ref branch of the disjunction and successfully unify.
This should work consistently regardless of whether the unification happens:
- Directly in a field assignment
- Inside a
forloop - Inside an
ifconditional - Via a pattern constraint
Actual Behavior
When the unification occurs inside a for loop or if conditional with an open struct (Properties?: {...}), CUE cannot determine which branch of the disjunction to take and reports:
incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}
The error suggests CUE is considering whether the value could have additional fields (like "Fn::Base64") due to the open struct, even though the concrete value {Ref: "DBCluster"} clearly only has a Ref field. The other thing that surprises me is the fact that it appears it is trying to unify the concrete {Ref:"DBCluster"} with a (closed) definition #Base64: {"Fn::Base64": string }
Minimal Reproduction
Case 1: Works ✅ (Pattern constraint workaround)
File: template-works.cue
package cloudformation
// Typed definitions
#Ref: {Ref: string}
#Base64: {
"Fn::Base64": string
}
// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64
stacks: "test-stack": {
let instances = [{Id: "01"}]
Resources: {
for instance in instances {
"DBInstance\(instance.Id)": {
Properties: DBClusterIdentifier: Ref: "DBCluster"
}
}
}
Resources: [=~"^DBInstance\\d+$"]: #DBInstance
}
#Resource: {
Properties?: {...}
}
#DBInstance: #Resource & {
Properties: DBClusterIdentifier?: (string) | #Fn
}Command: cue export template-works.cue
Result: ✅ Exports successfully
Note: The constraint is applied after the for loop via a pattern match. This allows the concrete value to be created first, then constrained.
Case 2: Fails ❌ (for loop with direct constraint)
File: template-breaks.cue
package cloudformation
// Typed definitions
#Ref: {Ref: string}
#Base64: {
"Fn::Base64": string
}
// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64
stacks: "test-stack": {
let instances = [{Id: "01"}]
Resources: {
for instance in instances {
"DBInstance\(instance.Id)": #DBInstance & {
Properties: DBClusterIdentifier: Ref: "DBCluster"
}
}
}
}
#Resource: {
Properties?: {...}
}
#DBInstance: #Resource & {
Properties: DBClusterIdentifier?: (string) | #Fn
}Command: cue export template-breaks.cue
Result: ❌ Fails with:
stacks."test-stack".Resources.DBInstance01.Properties.DBClusterIdentifier: incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}
Difference: The constraint #DBInstance is applied inside the for loop.
Case 3: Fails ❌ (if conditional with direct constraint)
File: template-breaks-if.cue
package cloudformation
// Typed definitions
#Ref: {Ref: string}
#Base64: {
"Fn::Base64": string
}
// The problematic union - 2 intrinsics
#Fn: #Ref | #Base64
stacks: "test-stack": {
let instances = [{Id: "01"}]
Resources: {
if instances != _|_ {
"DBInstance\(instances[0].Id)": #DBInstance & {
Properties: DBClusterIdentifier: Ref: "DBCluster"
}
}
}
}
#Resource: {
Properties?: {...}
}
#DBInstance: #Resource & {
Properties: DBClusterIdentifier?: (string) | #Fn
}Command: cue export template-breaks-if.cue
Result: ❌ Fails with:
stacks."test-stack".Resources.DBInstance01.Properties.DBClusterIdentifier: incomplete value {Ref:"DBCluster"} | {Ref:"DBCluster","Fn::Base64":string}
Difference: The constraint #DBInstance is applied inside an if conditional.
Root Cause Analysis
The issue appears to be related to how CUE handles disjunction resolution in the presence of:
- Open structs (
Properties?: {...}) - allows additional fields - Disjunctions (
#Fn: #Ref | #Base64) - multiple possible types - Control flow (
forloops orifconditionals) - delayed evaluation context
Additional Context
This issue was discovered while building a CloudFormation Infrastructure-as-Code library in CUE, where resource properties need to accept either literal values (strings) or intrinsic functions (various struct types like {Ref: string}, {"Fn::Base64": string}, etc.).
The current definition of #Resource with Properties being open is required because of the large number (~ 1450) of potential CloudFormation Resources types. If I create a disjunction of all of the possibilities, evaluation time gets rather hairy. I'll plan to submit a performance test for that soon.