Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions apis/meta/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,47 @@ limitations under the License.

package meta

import "strings"

// ObjectWithDependencies describes a Kubernetes resource object with dependencies.
// +k8s:deepcopy-gen=false
type ObjectWithDependencies interface {
// GetDependsOn returns a NamespacedObjectReference list the object depends on.
GetDependsOn() []NamespacedObjectReference
// GetDependsOn returns a DependencyReference list the object depends on.
GetDependsOn() []DependencyReference
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, and apis/meta is GA (v1.x). I'm ok with it, though, so I'll defer to other maintainers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stefanprodan This is the only item left open IMO

}

// MakeDependsOn parses a list of dependency strings into DependencyReference
// objects. Each dependency string can be in one of the following formats:
// - "name" - a dependency in the same namespace with no CEL expression
// - "namespace/name" - a dependency in a specific namespace
// - "name@readyExpr" - a dependency with a CEL readiness expression
// - "namespace/name@readyExpr" - a dependency in a specific namespace with a CEL expression
Comment thread
matheuscscp marked this conversation as resolved.
//
// The @ symbol is used to separate the resource reference from the CEL expression.
// Note that @ cannot be part of resource names or namespaces per Kubernetes naming conventions:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
// For CEL expression syntax, see:
// https://github.com/google/cel-spec/blob/master/doc/langdef.md
func MakeDependsOn(deps []string) []DependencyReference {
refs := make([]DependencyReference, 0, len(deps))
for _, dep := range deps {
ref := DependencyReference{}

// Split off the CEL ready expression if present.
if idx := strings.Index(dep, "@"); idx != -1 {
ref.ReadyExpr = dep[idx+1:]
dep = dep[:idx]
}

// Split the namespace/name.
if parts := strings.SplitN(dep, "/", 2); len(parts) == 2 {
ref.Namespace = parts[0]
ref.Name = parts[1]
} else {
ref.Name = dep
}

refs = append(refs, ref)
}
return refs
}
171 changes: 171 additions & 0 deletions apis/meta/dependencies_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
Copyright 2025 The Flux authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package meta_test

import (
"testing"

. "github.com/onsi/gomega"

"github.com/fluxcd/pkg/apis/meta"
)

func TestMakeDependsOn(t *testing.T) {
g := NewWithT(t)

tests := []struct {
name string
deps []string
want []meta.DependencyReference
}{
{
name: "empty",
deps: []string{},
want: []meta.DependencyReference{},
},
{
name: "single name only",
deps: []string{"redis"},
want: []meta.DependencyReference{
{Name: "redis"},
},
},
{
name: "single with namespace",
deps: []string{"default/redis"},
want: []meta.DependencyReference{
{Namespace: "default", Name: "redis"},
},
},
{
name: "single with CEL expression",
deps: []string{"redis@status.ready==true"},
want: []meta.DependencyReference{
{Name: "redis", ReadyExpr: "status.ready==true"},
},
},
{
name: "single with namespace and CEL expression",
deps: []string{"default/redis@status.ready==true"},
want: []meta.DependencyReference{
{Namespace: "default", Name: "redis", ReadyExpr: "status.ready==true"},
},
},
{
name: "multiple dependencies",
deps: []string{
"redis",
"default/postgres@status.ready==true",
"infra/cert-manager",
},
want: []meta.DependencyReference{
{Name: "redis"},
{Namespace: "default", Name: "postgres", ReadyExpr: "status.ready==true"},
{Namespace: "infra", Name: "cert-manager"},
},
},
{
name: "CEL expression with multiple operators",
deps: []string{"default/app@status.ready==true && status.observed==1"},
want: []meta.DependencyReference{
{Namespace: "default", Name: "app", ReadyExpr: "status.ready==true && status.observed==1"},
},
},
{
name: "CEL expression with function calls",
deps: []string{"infra/ingress@has(status.loadBalancer.ingress)"},
want: []meta.DependencyReference{
{Namespace: "infra", Name: "ingress", ReadyExpr: "has(status.loadBalancer.ingress)"},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := meta.MakeDependsOn(tt.deps)
g.Expect(got).To(Equal(tt.want))
})
}
}

func TestDependencyReferenceString(t *testing.T) {
g := NewWithT(t)

tests := []struct {
name string
ref meta.DependencyReference
want string
}{
{
name: "name only",
ref: meta.DependencyReference{Name: "redis"},
want: "redis",
},
{
name: "namespace and name",
ref: meta.DependencyReference{Namespace: "default", Name: "redis"},
want: "default/redis",
},
{
name: "name and CEL expression",
ref: meta.DependencyReference{Name: "redis", ReadyExpr: "status.ready==true"},
want: "redis@status.ready==true",
},
{
name: "namespace, name, and CEL expression",
ref: meta.DependencyReference{Namespace: "default", Name: "redis", ReadyExpr: "status.ready==true"},
want: "default/redis@status.ready==true",
},
{
name: "name with complex CEL expression",
ref: meta.DependencyReference{Name: "app", ReadyExpr: "status.ready==true && status.observed==1"},
want: "app@status.ready==true && status.observed==1",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.ref.String()
g.Expect(got).To(Equal(tt.want))
})
}
}

func TestDependencyReferenceRoundTrip(t *testing.T) {
g := NewWithT(t)

tests := []meta.DependencyReference{
{Name: "redis"},
{Namespace: "default", Name: "postgres"},
{Name: "cache", ReadyExpr: "status.ready==true"},
{Namespace: "infra", Name: "ingress", ReadyExpr: "has(status.loadBalancer.ingress)"},
}

for _, original := range tests {
t.Run(original.String(), func(t *testing.T) {
// Serialize to string
str := original.String()

// Parse back from string
parsed := meta.MakeDependsOn([]string{str})
g.Expect(parsed).To(HaveLen(1))

// Should match original
g.Expect(parsed[0]).To(Equal(original))
})
}
}
7 changes: 6 additions & 1 deletion apis/meta/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ module github.com/fluxcd/pkg/apis/meta

go 1.25.0

require k8s.io/apimachinery v0.35.2
require (
github.com/onsi/gomega v1.38.2
k8s.io/apimachinery v0.35.2
Comment thread
matheuscscp marked this conversation as resolved.
)

require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/text v0.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
Expand Down
20 changes: 20 additions & 0 deletions apis/meta/go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -16,6 +22,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
Expand All @@ -28,10 +38,20 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
Expand Down
35 changes: 35 additions & 0 deletions apis/meta/reference_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,41 @@ func (in NamespacedObjectReference) String() string {
return in.Name
}

// DependencyReference contains enough information to locate the referenced Kubernetes resource object
// and optional CEL expression to assess its readiness.
type DependencyReference struct {
// Name of the referent.
// +required
Name string `json:"name"`

// Namespace of the referent, defaults to the namespace of the resource
// object that contains the reference.
// +optional
Namespace string `json:"namespace,omitempty"`

// ReadyExpr is a CEL expression that can be used to assess the readiness
// of a dependency. When specified, the built-in readiness check
// is replaced by the logic defined in the CEL expression.
// To make the CEL expression additive to the built-in readiness check,
// the feature gate `AdditiveCELDependencyCheck` must be set to `true`.
// +optional
ReadyExpr string `json:"readyExpr,omitempty"`
}

// String implements the fmt.Stringer interface for DependencyReference.
// Returns the dependency reference in the format: [namespace/]name[@readyExpr]
// Examples: "app", "ns/app", "app@ready", "ns/app@obj.status.ready"
func (in DependencyReference) String() string {
Comment thread
matheuscscp marked this conversation as resolved.
s := in.Name
if in.Namespace != "" {
s = in.Namespace + "/" + s
}
if in.ReadyExpr != "" {
s = s + "@" + in.ReadyExpr
}
return s
}

// NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object
// in any namespace.
type NamespacedObjectKindReference struct {
Expand Down
15 changes: 15 additions & 0 deletions apis/meta/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion runtime/dependency/sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ func Sort(objects []Dependent) ([]meta.NamespacedObjectReference, error) {
Namespace: obj.GetNamespace(),
}
vertices = append(vertices, u)
for _, v := range obj.GetDependsOn() {
for _, depRef := range obj.GetDependsOn() {
v := meta.NamespacedObjectReference{
Comment thread
matheuscscp marked this conversation as resolved.
Name: depRef.Name,
Namespace: depRef.Namespace,
}
if v.Namespace == "" {
v.Namespace = obj.GetNamespace()
}
Expand Down
Loading
Loading