Skip to content

Commit bbccaa6

Browse files
refactor: move DependencyReference to apis/meta
Signed-off-by: Iam-karan-suresh <karansuresh.info@gmail.com> Signed-off-by: iam-karan-suresh <karansuresh.info@gmail.com>
1 parent ad86bcd commit bbccaa6

8 files changed

Lines changed: 299 additions & 17 deletions

File tree

apis/meta/dependencies.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,41 @@ limitations under the License.
1616

1717
package meta
1818

19+
import "strings"
20+
1921
// ObjectWithDependencies describes a Kubernetes resource object with dependencies.
2022
// +k8s:deepcopy-gen=false
2123
type ObjectWithDependencies interface {
22-
// GetDependsOn returns a NamespacedObjectReference list the object depends on.
23-
GetDependsOn() []NamespacedObjectReference
24+
// GetDependsOn returns a DependencyReference list the object depends on.
25+
GetDependsOn() []DependencyReference
26+
}
27+
28+
// MakeDependsOn parses a list of dependency strings into DependencyReference
29+
// objects. Each dependency string can be in one of the following formats:
30+
// - "name" - a dependency in the same namespace with no CEL expression
31+
// - "namespace/name" - a dependency in a specific namespace
32+
// - "name@readyExpr" - a dependency with a CEL readiness expression
33+
// - "namespace/name@readyExpr" - a dependency in a specific namespace with a CEL expression
34+
func MakeDependsOn(deps []string) []DependencyReference {
35+
refs := make([]DependencyReference, 0, len(deps))
36+
for _, dep := range deps {
37+
ref := DependencyReference{}
38+
39+
// Split off the CEL ready expression if present.
40+
if idx := strings.Index(dep, "@"); idx != -1 {
41+
ref.ReadyExpr = dep[idx+1:]
42+
dep = dep[:idx]
43+
}
44+
45+
// Split the namespace/name.
46+
if parts := strings.SplitN(dep, "/", 2); len(parts) == 2 {
47+
ref.Namespace = parts[0]
48+
ref.Name = parts[1]
49+
} else {
50+
ref.Name = dep
51+
}
52+
53+
refs = append(refs, ref)
54+
}
55+
return refs
2456
}

apis/meta/dependencies_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package meta_test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
24+
"github.com/fluxcd/pkg/apis/meta"
25+
)
26+
27+
func TestMakeDependsOn(t *testing.T) {
28+
g := NewWithT(t)
29+
30+
tests := []struct {
31+
name string
32+
deps []string
33+
want []meta.DependencyReference
34+
}{
35+
{
36+
name: "empty",
37+
deps: []string{},
38+
want: []meta.DependencyReference{},
39+
},
40+
{
41+
name: "single name only",
42+
deps: []string{"redis"},
43+
want: []meta.DependencyReference{
44+
{Name: "redis"},
45+
},
46+
},
47+
{
48+
name: "single with namespace",
49+
deps: []string{"default/redis"},
50+
want: []meta.DependencyReference{
51+
{Namespace: "default", Name: "redis"},
52+
},
53+
},
54+
{
55+
name: "single with CEL expression",
56+
deps: []string{"redis@status.ready==true"},
57+
want: []meta.DependencyReference{
58+
{Name: "redis", ReadyExpr: "status.ready==true"},
59+
},
60+
},
61+
{
62+
name: "single with namespace and CEL expression",
63+
deps: []string{"default/redis@status.ready==true"},
64+
want: []meta.DependencyReference{
65+
{Namespace: "default", Name: "redis", ReadyExpr: "status.ready==true"},
66+
},
67+
},
68+
{
69+
name: "multiple dependencies",
70+
deps: []string{
71+
"redis",
72+
"default/postgres@status.ready==true",
73+
"infra/cert-manager",
74+
},
75+
want: []meta.DependencyReference{
76+
{Name: "redis"},
77+
{Namespace: "default", Name: "postgres", ReadyExpr: "status.ready==true"},
78+
{Namespace: "infra", Name: "cert-manager"},
79+
},
80+
},
81+
{
82+
name: "CEL expression with multiple operators",
83+
deps: []string{"default/app@status.ready==true && status.observed==1"},
84+
want: []meta.DependencyReference{
85+
{Namespace: "default", Name: "app", ReadyExpr: "status.ready==true && status.observed==1"},
86+
},
87+
},
88+
{
89+
name: "CEL expression with function calls",
90+
deps: []string{"infra/ingress@has(status.loadBalancer.ingress)"},
91+
want: []meta.DependencyReference{
92+
{Namespace: "infra", Name: "ingress", ReadyExpr: "has(status.loadBalancer.ingress)"},
93+
},
94+
},
95+
}
96+
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
got := meta.MakeDependsOn(tt.deps)
100+
g.Expect(got).To(Equal(tt.want))
101+
})
102+
}
103+
}
104+
105+
func TestDependencyReferenceString(t *testing.T) {
106+
g := NewWithT(t)
107+
108+
tests := []struct {
109+
name string
110+
ref meta.DependencyReference
111+
want string
112+
}{
113+
{
114+
name: "name only",
115+
ref: meta.DependencyReference{Name: "redis"},
116+
want: "redis",
117+
},
118+
{
119+
name: "namespace and name",
120+
ref: meta.DependencyReference{Namespace: "default", Name: "redis"},
121+
want: "default/redis",
122+
},
123+
{
124+
name: "name and CEL expression",
125+
ref: meta.DependencyReference{Name: "redis", ReadyExpr: "status.ready==true"},
126+
want: "redis@status.ready==true",
127+
},
128+
{
129+
name: "namespace, name, and CEL expression",
130+
ref: meta.DependencyReference{Namespace: "default", Name: "redis", ReadyExpr: "status.ready==true"},
131+
want: "default/redis@status.ready==true",
132+
},
133+
{
134+
name: "name with complex CEL expression",
135+
ref: meta.DependencyReference{Name: "app", ReadyExpr: "status.ready==true && status.observed==1"},
136+
want: "app@status.ready==true && status.observed==1",
137+
},
138+
}
139+
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
got := tt.ref.String()
143+
g.Expect(got).To(Equal(tt.want))
144+
})
145+
}
146+
}
147+
148+
func TestDependencyReferenceRoundTrip(t *testing.T) {
149+
g := NewWithT(t)
150+
151+
tests := []meta.DependencyReference{
152+
{Name: "redis"},
153+
{Namespace: "default", Name: "postgres"},
154+
{Name: "cache", ReadyExpr: "status.ready==true"},
155+
{Namespace: "infra", Name: "ingress", ReadyExpr: "has(status.loadBalancer.ingress)"},
156+
}
157+
158+
for _, original := range tests {
159+
t.Run(original.String(), func(t *testing.T) {
160+
// Serialize to string
161+
str := original.String()
162+
163+
// Parse back from string
164+
parsed := meta.MakeDependsOn([]string{str})
165+
g.Expect(parsed).To(HaveLen(1))
166+
167+
// Should match original
168+
g.Expect(parsed[0]).To(Equal(original))
169+
})
170+
}
171+
}

apis/meta/go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ module github.com/fluxcd/pkg/apis/meta
22

33
go 1.25.0
44

5-
require k8s.io/apimachinery v0.35.2
5+
require (
6+
github.com/onsi/gomega v1.38.2
7+
k8s.io/apimachinery v0.35.2
8+
)
69

710
require (
811
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
912
github.com/go-logr/logr v1.4.3 // indirect
13+
github.com/google/go-cmp v0.7.0 // indirect
1014
github.com/json-iterator/go v1.1.12 // indirect
1115
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
1216
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
1317
github.com/x448/float16 v0.8.4 // indirect
1418
go.yaml.in/yaml/v2 v2.4.3 // indirect
19+
go.yaml.in/yaml/v3 v3.0.4 // indirect
1520
golang.org/x/net v0.49.0 // indirect
1621
golang.org/x/text v0.33.0 // indirect
1722
gopkg.in/inf.v0 v0.9.1 // indirect

apis/meta/go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
2+
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
13
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
46
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
57
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
68
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
79
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
10+
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
11+
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
812
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
913
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
1014
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
15+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
16+
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
1117
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
1218
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
1319
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -16,6 +22,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
1622
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
1723
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
1824
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
25+
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
26+
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
27+
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
28+
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
1929
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2030
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2131
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
@@ -28,10 +38,20 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
2838
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
2939
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
3040
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
41+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
42+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
43+
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
44+
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
3145
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
3246
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
47+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
48+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
49+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
50+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
3351
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
3452
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
53+
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
54+
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
3555
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
3656
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3757
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=

apis/meta/reference_types.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,41 @@ func (in NamespacedObjectReference) String() string {
4343
return in.Name
4444
}
4545

46+
// DependencyReference contains enough information to locate the referenced Kubernetes resource object
47+
// and optional CEL expression to assess its readiness.
48+
type DependencyReference struct {
49+
// Name of the referent.
50+
// +required
51+
Name string `json:"name"`
52+
53+
// Namespace of the referent, defaults to the namespace of the resource
54+
// object that contains the reference.
55+
// +optional
56+
Namespace string `json:"namespace,omitempty"`
57+
58+
// ReadyExpr is a CEL expression that can be used to assess the readiness
59+
// of a dependency. When specified, the built-in readiness check
60+
// is replaced by the logic defined in the CEL expression.
61+
// To make the CEL expression additive to the built-in readiness check,
62+
// the feature gate `AdditiveCELDependencyCheck` must be set to `true`.
63+
// +optional
64+
ReadyExpr string `json:"readyExpr,omitempty"`
65+
}
66+
67+
// String implements the fmt.Stringer interface for DependencyReference.
68+
// Returns the dependency reference in the format: [namespace/]name[@readyExpr]
69+
// Examples: "app", "ns/app", "app@ready", "ns/app@obj.status.ready"
70+
func (in DependencyReference) String() string {
71+
s := in.Name
72+
if in.Namespace != "" {
73+
s = in.Namespace + "/" + s
74+
}
75+
if in.ReadyExpr != "" {
76+
s = s + "@" + in.ReadyExpr
77+
}
78+
return s
79+
}
80+
4681
// NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object
4782
// in any namespace.
4883
type NamespacedObjectKindReference struct {

apis/meta/zz_generated.deepcopy.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/dependency/sort.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ func Sort(objects []Dependent) ([]meta.NamespacedObjectReference, error) {
5656
Namespace: obj.GetNamespace(),
5757
}
5858
vertices = append(vertices, u)
59-
for _, v := range obj.GetDependsOn() {
59+
for _, depRef := range obj.GetDependsOn() {
60+
v := meta.NamespacedObjectReference{
61+
Name: depRef.Name,
62+
Namespace: depRef.Namespace,
63+
}
6064
if v.Namespace == "" {
6165
v.Namespace = obj.GetNamespace()
6266
}

0 commit comments

Comments
 (0)