Skip to content

Commit b4128a9

Browse files
fix: remove status fields from the list of required fields before type-checking
Remove status from required fields before type-checking. This allows CEL expressions in dependent resources to reference instance status (e.g., `${schema.status.endpoint}`) without validation errors when the instance status hasn't been populated yet. Signed-off-by: Bruno Schaatsbergen <[email protected]>
1 parent 4c718eb commit b4128a9

File tree

2 files changed

+146
-1
lines changed

2 files changed

+146
-1
lines changed

pkg/graph/builder.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,24 @@ func validateReadyWhenExpressions(env *cel.Env, resource *Resource) error {
829829
return nil
830830
}
831831

832+
// unrequireStatus modifies the schema to ensure "status" is not in the required fields list.
833+
// This prevents CEL type-checking from treating status as a required field, which would incorrectly
834+
// fail validation when status is legitimately absent (e.g., resources that haven't reconciled yet).
835+
func unrequireStatus(s *spec.Schema) {
836+
if s == nil || len(s.Required) == 0 {
837+
return
838+
}
839+
840+
// Remove "status" from required fields if present
841+
required := make([]string, 0, len(s.Required))
842+
for _, field := range s.Required {
843+
if field != "status" {
844+
required = append(required, field)
845+
}
846+
}
847+
s.Required = required
848+
}
849+
832850
// getInstanceSchema returns a schema from the CRD including spec, status, and metadata fields.
833851
// This schema is used for CEL expression validation, allowing references to instance fields like
834852
// ${schema.spec.field}, ${schema.status.field}, and ${schema.metadata.name}.
@@ -855,5 +873,11 @@ func getInstanceSchema(crd *extv1.CustomResourceDefinition) (*spec.Schema, error
855873
specSchema.Properties = make(map[string]spec.Schema)
856874
}
857875
specSchema.Properties["metadata"] = schema.ObjectMetaSchema
876+
877+
// Unrequire status before type-checking. This allows CEL expressions
878+
// in dependent resources to reference instance status (e.g., ${schema.status.endpoint})
879+
// without validation errors when the instance status hasn't been populated yet.
880+
unrequireStatus(specSchema)
881+
858882
return specSchema, nil
859883
}

pkg/graph/builder_test.go

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,7 @@ func TestGraphBuilder_CELTypeChecking(t *testing.T) {
17671767
wantErr: false,
17681768
},
17691769
{
1770-
name: "valid schema.status field reference",
1770+
name: "resource references instance status field (string)",
17711771
resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
17721772
generator.WithSchema(
17731773
"Application", "v1alpha1",
@@ -1802,6 +1802,127 @@ func TestGraphBuilder_CELTypeChecking(t *testing.T) {
18021802
},
18031803
wantErr: false,
18041804
},
1805+
{
1806+
name: "resource references instance status field (nested object)",
1807+
resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
1808+
generator.WithSchema(
1809+
"NetworkApp", "v1alpha1",
1810+
map[string]interface{}{
1811+
"vpcName": "string",
1812+
},
1813+
map[string]interface{}{
1814+
"vpcState": "${vpc.status.state}",
1815+
},
1816+
),
1817+
generator.WithResource("vpc", map[string]interface{}{
1818+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1819+
"kind": "VPC",
1820+
"metadata": map[string]interface{}{
1821+
"name": "${schema.spec.vpcName}",
1822+
},
1823+
"spec": map[string]interface{}{
1824+
"cidrBlocks": []interface{}{"10.0.0.0/16"},
1825+
},
1826+
}, nil, nil),
1827+
generator.WithResource("securitygroup", map[string]interface{}{
1828+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1829+
"kind": "SecurityGroup",
1830+
"metadata": map[string]interface{}{
1831+
"name": "${schema.spec.vpcName}-sg",
1832+
},
1833+
"spec": map[string]interface{}{
1834+
"description": "VPC state is: ${schema.status.vpcState}",
1835+
},
1836+
}, nil, nil),
1837+
},
1838+
wantErr: false,
1839+
},
1840+
{
1841+
name: "multiple resources reference same instance status field",
1842+
resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
1843+
generator.WithSchema(
1844+
"Application", "v1alpha1",
1845+
map[string]interface{}{
1846+
"region": "string",
1847+
},
1848+
map[string]interface{}{
1849+
"primaryVPCID": "${vpc.status.vpcID}",
1850+
},
1851+
),
1852+
generator.WithResource("vpc", map[string]interface{}{
1853+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1854+
"kind": "VPC",
1855+
"metadata": map[string]interface{}{
1856+
"name": "primary-vpc",
1857+
},
1858+
"spec": map[string]interface{}{
1859+
"cidrBlocks": []interface{}{"10.0.0.0/16"},
1860+
},
1861+
}, nil, nil),
1862+
generator.WithResource("subnet1", map[string]interface{}{
1863+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1864+
"kind": "Subnet",
1865+
"metadata": map[string]interface{}{
1866+
"name": "subnet-1",
1867+
},
1868+
"spec": map[string]interface{}{
1869+
"cidrBlock": "10.0.1.0/24",
1870+
"vpcID": "${schema.status.primaryVPCID}",
1871+
},
1872+
}, nil, nil),
1873+
generator.WithResource("subnet2", map[string]interface{}{
1874+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1875+
"kind": "Subnet",
1876+
"metadata": map[string]interface{}{
1877+
"name": "subnet-2",
1878+
},
1879+
"spec": map[string]interface{}{
1880+
"cidrBlock": "10.0.2.0/24",
1881+
"vpcID": "${schema.status.primaryVPCID}",
1882+
},
1883+
}, nil, nil),
1884+
},
1885+
wantErr: false,
1886+
},
1887+
{
1888+
name: "resource references instance status field (boolean computed)",
1889+
resourceGraphDefinitionOpts: []generator.ResourceGraphDefinitionOption{
1890+
generator.WithSchema(
1891+
"Application", "v1alpha1",
1892+
map[string]interface{}{
1893+
"region": "string",
1894+
},
1895+
map[string]interface{}{
1896+
"vpcReady": "${vpc.status.state == 'available'}",
1897+
},
1898+
),
1899+
generator.WithResource("vpc", map[string]interface{}{
1900+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1901+
"kind": "VPC",
1902+
"metadata": map[string]interface{}{
1903+
"name": "app-vpc",
1904+
},
1905+
"spec": map[string]interface{}{
1906+
"cidrBlocks": []interface{}{"10.0.0.0/16"},
1907+
},
1908+
}, nil, nil),
1909+
generator.WithResource("subnet", map[string]interface{}{
1910+
"apiVersion": "ec2.services.k8s.aws/v1alpha1",
1911+
"kind": "Subnet",
1912+
"metadata": map[string]interface{}{
1913+
"name": "conditional-subnet",
1914+
"labels": map[string]interface{}{
1915+
"vpc-ready": "${schema.status.vpcReady ? 'true' : 'false'}",
1916+
},
1917+
},
1918+
"spec": map[string]interface{}{
1919+
"cidrBlock": "10.0.1.0/24",
1920+
"vpcID": "${vpc.status.vpcID}",
1921+
},
1922+
}, nil, nil),
1923+
},
1924+
wantErr: false,
1925+
},
18051926
}
18061927

18071928
for _, tt := range tests {

0 commit comments

Comments
 (0)