From 5533f3bc8682129dc36ca232a60839cc1518225b Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Tue, 23 Jun 2026 00:20:26 +1000 Subject: [PATCH 1/7] feat: refactor: use tree-based CEL building for the VAP manager (migrating to VAP 3/) (#736) --- pkg/admissionpolicymanager/cel.go | 194 +++++++ pkg/admissionpolicymanager/cel_test.go | 532 ++++++++++++++++++ pkg/admissionpolicymanager/commons.go | 13 + pkg/admissionpolicymanager/manager.go | 7 +- .../manager_integration_test.go | 4 +- .../podsnreplicasets.go | 17 +- .../svcaccountsntokenreqs.go | 68 ++- 7 files changed, 797 insertions(+), 38 deletions(-) create mode 100644 pkg/admissionpolicymanager/cel.go create mode 100644 pkg/admissionpolicymanager/cel_test.go diff --git a/pkg/admissionpolicymanager/cel.go b/pkg/admissionpolicymanager/cel.go new file mode 100644 index 000000000..64bba9b1b --- /dev/null +++ b/pkg/admissionpolicymanager/cel.go @@ -0,0 +1,194 @@ +/* +Copyright 2026 The KubeFleet 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 admissionpolicymanager + +import ( + "fmt" + "strings" +) + +// CELExprTreeNode is the interface for CEL tree nodes. It helps build CEL expressions in a +// structured way. +type CELExprTreeNode interface { + // Build returns the CEL expression represented by the node and its children. + Build() (string, error) + // Children returns the child nodes of the current node. + Children() []CELExprTreeNode +} + +// CELExprTreeEditableNode is the interface for CEL tree nodes that can be edited (i.e., have child nodes added to them). +// +// This is added to simplify cases where a CEL tree node need to be built step-by-step. +type CELExprTreeEditableNode interface { + CELExprTreeNode + // Add adds a child node to the current node. + Add(child CELExprTreeNode) +} + +var ( + // Verify that all node types implement the CELExprTreeNode interface. + _ CELExprTreeNode = &rawCELExprTreeNode{} + _ CELExprTreeNode = &orCELExprTreeNode{} + _ CELExprTreeNode = &andCELExprTreeNode{} + _ CELExprTreeNode = ¬CELExprTreeNode{} + + _ CELExprTreeEditableNode = &orCELExprTreeNode{} + _ CELExprTreeEditableNode = &andCELExprTreeNode{} +) + +// rawCELExprTreeNode represents a raw CEL expression. It is a leaf node in the CEL expression tree. +type rawCELExprTreeNode struct { + expr string +} + +// Build returns the raw CEL expression. +func (n *rawCELExprTreeNode) Build() (string, error) { + if len(n.expr) == 0 { + return "", fmt.Errorf("raw CEL expression cannot be empty") + } + return n.expr, nil +} + +// Children returns the child nodes of the current node. +// +// For rawCELExprTreeNode, it always returns nil. +func (n *rawCELExprTreeNode) Children() []CELExprTreeNode { + return nil +} + +// RawCELExpr returns a new rawCELExprTreeNode with the given expression. +func RawCELExpr(expr string) CELExprTreeNode { + return &rawCELExprTreeNode{expr: expr} +} + +// orCELExprTreeNode represents a logical OR of its child nodes' expressions. +type orCELExprTreeNode struct { + children []CELExprTreeNode +} + +// Build returns the CEL expression representing the logical OR of its child nodes' expressions. +func (n *orCELExprTreeNode) Build() (string, error) { + exprs := make([]string, len(n.children)) + for i, child := range n.children { + if child == nil { + return "", fmt.Errorf("a child node is nil") + } + expr, err := child.Build() + if err != nil { + return "", fmt.Errorf("failed to build child node: %w", err) + } + if len(expr) == 0 { + return "", fmt.Errorf("a child node built to an empty expression") + } + exprs[i] = fmt.Sprintf("(%s)", expr) + } + + res := strings.Join(exprs, " || ") + if len(res) == 0 { + return "", fmt.Errorf("built to an empty expression") + } + return res, nil +} + +// Children returns the child nodes of the current node. +func (n *orCELExprTreeNode) Children() []CELExprTreeNode { + return n.children +} + +// Add adds a child node to the current node. +func (n *orCELExprTreeNode) Add(child CELExprTreeNode) { + n.children = append(n.children, child) +} + +// LogicalOr returns a new orCELExprTreeNode with the given child nodes. +func LogicalOr(children ...CELExprTreeNode) CELExprTreeEditableNode { + return &orCELExprTreeNode{children: children} +} + +// andCELExprTreeNode represents a logical AND of its child nodes' expressions. +type andCELExprTreeNode struct { + children []CELExprTreeNode +} + +// Build returns the CEL expression representing the logical AND of its child nodes' expressions. +func (n *andCELExprTreeNode) Build() (string, error) { + exprs := make([]string, len(n.children)) + for i, child := range n.children { + if child == nil { + return "", fmt.Errorf("a child node is nil") + } + expr, err := child.Build() + if err != nil { + return "", fmt.Errorf("failed to build child node: %w", err) + } + if len(expr) == 0 { + return "", fmt.Errorf("a child node built to an empty expression") + } + exprs[i] = fmt.Sprintf("(%s)", expr) + } + + res := strings.Join(exprs, " && ") + if len(res) == 0 { + return "", fmt.Errorf("built to an empty expression") + } + return res, nil +} + +// Children returns the child nodes of the current node. +func (n *andCELExprTreeNode) Children() []CELExprTreeNode { + return n.children +} + +// Add adds a child node to the current node. +func (n *andCELExprTreeNode) Add(child CELExprTreeNode) { + n.children = append(n.children, child) +} + +// LogicalAnd returns a new andCELExprTreeNode with the given child nodes. +func LogicalAnd(children ...CELExprTreeNode) CELExprTreeEditableNode { + return &andCELExprTreeNode{children: children} +} + +// notCELExprTreeNode represents a logical NOT of its child node's expression. +type notCELExprTreeNode struct { + child CELExprTreeNode +} + +// Build returns the CEL expression representing the logical NOT of its child node's expression. +func (n *notCELExprTreeNode) Build() (string, error) { + if n.child == nil { + return "", fmt.Errorf("child node is nil") + } + expr, err := n.child.Build() + if err != nil { + return "", fmt.Errorf("failed to build child node: %w", err) + } + if len(expr) == 0 { + return "", fmt.Errorf("child node built to an empty expression") + } + return fmt.Sprintf("!(%s)", expr), nil +} + +// Children returns the child node of the current node. +func (n *notCELExprTreeNode) Children() []CELExprTreeNode { + return []CELExprTreeNode{n.child} +} + +// LogicalNot returns a new notCELExprTreeNode with the given child node. +func LogicalNot(child CELExprTreeNode) CELExprTreeNode { + return ¬CELExprTreeNode{child: child} +} diff --git a/pkg/admissionpolicymanager/cel_test.go b/pkg/admissionpolicymanager/cel_test.go new file mode 100644 index 000000000..e92c546f6 --- /dev/null +++ b/pkg/admissionpolicymanager/cel_test.go @@ -0,0 +1,532 @@ +/* +Copyright 2026 The KubeFleet 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 admissionpolicymanager + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +// TestCELExprTreeNode_Raw tests the RawCELExprTreeNode implementation of the CELExprTreeNode interface. +func TestCELExprTreeNode_Raw(t *testing.T) { + testCases := []struct { + name string + expr string + wantBuilt string + wantErred bool + wantErrSubstring string + }{ + { + name: "valid", + expr: "x = y", + wantBuilt: "x = y", + wantErred: false, + }, + { + name: "empty expression", + expr: "", + wantBuilt: "", + wantErred: true, + wantErrSubstring: "raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := RawCELExpr(tc.expr) + + gotBuilt, err := node.Build() + if tc.wantErred { + if err == nil { + t.Fatal("Build() = nil, want erred") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + return + } + if err != nil { + t.Fatalf("Build() = %v, want no error", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + + gotChildren := node.Children() + wantChildren := []CELExprTreeNode(nil) + if !cmp.Equal(gotChildren, wantChildren) { + t.Errorf("Children() = %v, want %v", gotChildren, wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalOr tests the orCELExprTreeNode implementation of the CELExprTreeNode interface. +func TestCELExprTreeNode_LogicalOr(t *testing.T) { + testCases := []struct { + name string + children []CELExprTreeNode + wantBuilt string + wantChildren []CELExprTreeNode + }{ + { + name: "single child", + children: []CELExprTreeNode{RawCELExpr("x = y")}, + wantBuilt: "(x = y)", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y")}, + }, + { + name: "multiple children", + children: []CELExprTreeNode{ + RawCELExpr("x = y"), + RawCELExpr("y = z"), + RawCELExpr("z = a"), + }, + wantBuilt: "(x = y) || (y = z) || (z = a)", + wantChildren: []CELExprTreeNode{ + RawCELExpr("x = y"), + RawCELExpr("y = z"), + RawCELExpr("z = a"), + }, + }, + { + name: "nested child", + children: []CELExprTreeNode{ + RawCELExpr("x = y"), + LogicalAnd(RawCELExpr("y = z"), RawCELExpr("z = a")), + }, + wantBuilt: "(x = y) || ((y = z) && (z = a))", + wantChildren: []CELExprTreeNode{ + RawCELExpr("x = y"), + LogicalAnd(RawCELExpr("y = z"), RawCELExpr("z = a")), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalOr(tc.children...) + + gotBuilt, err := node.Build() + if err != nil { + t.Fatalf("Build() = %v", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + + gotChildren := node.Children() + if !cmp.Equal(gotChildren, tc.wantChildren, + cmp.AllowUnexported(rawCELExprTreeNode{}), + cmp.AllowUnexported(andCELExprTreeNode{}), + cmp.AllowUnexported(orCELExprTreeNode{}), + cmp.AllowUnexported(notCELExprTreeNode{}), + ) { + t.Errorf("Children() = %v, want %v", gotChildren, tc.wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalOr_Erred tests LogicalOr error scenarios. +func TestCELExprTreeNode_LogicalOr_Erred(t *testing.T) { + testCases := []struct { + name string + children []CELExprTreeNode + wantErrSubstring string + }{ + { + name: "no children", + children: nil, + wantErrSubstring: "built to an empty expression", + }, + { + name: "nil child", + children: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantErrSubstring: "a child node is nil", + }, + { + name: "child parse error", + children: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantErrSubstring: "failed to build child node: raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalOr(tc.children...) + + gotBuilt, err := node.Build() + if err == nil { + t.Fatalf("Build() = nil, want error") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + if gotBuilt != "" { + t.Errorf("Build() = %v, want empty string on error", gotBuilt) + } + }) + } +} + +// TestCELExprTreeNode_LogicalOr_Add tests Add behavior for LogicalOr nodes. +func TestCELExprTreeNode_LogicalOr_Add(t *testing.T) { + testCases := []struct { + name string + childrenToAdd []CELExprTreeNode + wantBuilt string + wantChildren []CELExprTreeNode + wantErred bool + wantErrSubstring string + }{ + { + name: "append valid children", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("y = z")}, + wantBuilt: "(x = y) || (y = z)", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("y = z")}, + wantErred: false, + }, + { + name: "append nil child", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantBuilt: "", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantErred: true, + wantErrSubstring: "a child node is nil", + }, + { + name: "append child that fails parse", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantBuilt: "", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantErred: true, + wantErrSubstring: "failed to build child node: raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + orNode := LogicalOr() + + for _, child := range tc.childrenToAdd { + orNode.Add(child) + } + + gotBuilt, err := orNode.Build() + if tc.wantErred { + if err == nil { + t.Fatal("Build() = nil, want erred") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + } else { + if err != nil { + t.Fatalf("Build() = %v, want no error", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + } + + gotChildren := orNode.Children() + if !cmp.Equal(gotChildren, tc.wantChildren, + cmp.AllowUnexported(rawCELExprTreeNode{}), + cmp.AllowUnexported(andCELExprTreeNode{}), + cmp.AllowUnexported(orCELExprTreeNode{}), + cmp.AllowUnexported(notCELExprTreeNode{}), + ) { + t.Errorf("Children() = %v, want %v", gotChildren, tc.wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalAnd tests the andCELExprTreeNode implementation of the CELExprTreeNode interface. +func TestCELExprTreeNode_LogicalAnd(t *testing.T) { + testCases := []struct { + name string + children []CELExprTreeNode + wantBuilt string + wantChildren []CELExprTreeNode + }{ + { + name: "single child", + children: []CELExprTreeNode{RawCELExpr("x = y")}, + wantBuilt: "(x = y)", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y")}, + }, + { + name: "multiple children", + children: []CELExprTreeNode{ + RawCELExpr("x = y"), + RawCELExpr("y = z"), + RawCELExpr("z = a"), + }, + wantBuilt: "(x = y) && (y = z) && (z = a)", + wantChildren: []CELExprTreeNode{ + RawCELExpr("x = y"), + RawCELExpr("y = z"), + RawCELExpr("z = a"), + }, + }, + { + name: "nested child", + children: []CELExprTreeNode{ + RawCELExpr("x = y"), + LogicalOr(RawCELExpr("y = z"), RawCELExpr("z = a")), + }, + wantBuilt: "(x = y) && ((y = z) || (z = a))", + wantChildren: []CELExprTreeNode{ + RawCELExpr("x = y"), + LogicalOr(RawCELExpr("y = z"), RawCELExpr("z = a")), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalAnd(tc.children...) + + gotBuilt, err := node.Build() + if err != nil { + t.Fatalf("Build() = %v", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + + gotChildren := node.Children() + if !cmp.Equal(gotChildren, tc.wantChildren, + cmp.AllowUnexported(rawCELExprTreeNode{}), + cmp.AllowUnexported(andCELExprTreeNode{}), + cmp.AllowUnexported(orCELExprTreeNode{}), + cmp.AllowUnexported(notCELExprTreeNode{}), + ) { + t.Errorf("Children() = %v, want %v", gotChildren, tc.wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalAnd_Erred tests LogicalAnd error scenarios. +func TestCELExprTreeNode_LogicalAnd_Erred(t *testing.T) { + testCases := []struct { + name string + children []CELExprTreeNode + wantErrSubstring string + }{ + { + name: "no children", + children: nil, + wantErrSubstring: "built to an empty expression", + }, + { + name: "nil child", + children: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantErrSubstring: "a child node is nil", + }, + { + name: "child parse error", + children: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantErrSubstring: "failed to build child node: raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalAnd(tc.children...) + + gotBuilt, err := node.Build() + if err == nil { + t.Fatalf("Build() = nil, want error") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + if gotBuilt != "" { + t.Errorf("Build() = %v, want empty string on error", gotBuilt) + } + }) + } +} + +// TestCELExprTreeNode_LogicalAnd_Add tests Add behavior for LogicalAnd nodes. +func TestCELExprTreeNode_LogicalAnd_Add(t *testing.T) { + testCases := []struct { + name string + childrenToAdd []CELExprTreeNode + wantBuilt string + wantChildren []CELExprTreeNode + wantErred bool + wantErrSubstring string + }{ + { + name: "append valid children", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("y = z")}, + wantBuilt: "(x = y) && (y = z)", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("y = z")}, + wantErred: false, + }, + { + name: "append nil child", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantBuilt: "", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), nil}, + wantErred: true, + wantErrSubstring: "a child node is nil", + }, + { + name: "append child that fails parse", + childrenToAdd: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantBuilt: "", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y"), RawCELExpr("")}, + wantErred: true, + wantErrSubstring: "failed to build child node: raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + andNode := LogicalAnd() + + for _, child := range tc.childrenToAdd { + andNode.Add(child) + } + + gotBuilt, err := andNode.Build() + if tc.wantErred { + if err == nil { + t.Fatal("Build() = nil, want erred") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + } else { + if err != nil { + t.Fatalf("Build() = %v, want no error", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + } + + gotChildren := andNode.Children() + if !cmp.Equal(gotChildren, tc.wantChildren, + cmp.AllowUnexported(rawCELExprTreeNode{}), + cmp.AllowUnexported(andCELExprTreeNode{}), + cmp.AllowUnexported(orCELExprTreeNode{}), + cmp.AllowUnexported(notCELExprTreeNode{}), + ) { + t.Errorf("Children() = %v, want %v", gotChildren, tc.wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalNot tests the notCELExprTreeNode implementation of the CELExprTreeNode interface. +func TestCELExprTreeNode_LogicalNot(t *testing.T) { + testCases := []struct { + name string + child CELExprTreeNode + wantBuilt string + wantChildren []CELExprTreeNode + }{ + { + name: "raw child", + child: RawCELExpr("x = y"), + wantBuilt: "!(x = y)", + wantChildren: []CELExprTreeNode{RawCELExpr("x = y")}, + }, + { + name: "and child", + child: LogicalAnd(RawCELExpr("x = y"), RawCELExpr("y = z")), + wantBuilt: "!((x = y) && (y = z))", + wantChildren: []CELExprTreeNode{LogicalAnd(RawCELExpr("x = y"), RawCELExpr("y = z"))}, + }, + { + name: "or child", + child: LogicalOr(RawCELExpr("x = y"), RawCELExpr("y = z")), + wantBuilt: "!((x = y) || (y = z))", + wantChildren: []CELExprTreeNode{LogicalOr(RawCELExpr("x = y"), RawCELExpr("y = z"))}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalNot(tc.child) + + gotBuilt, err := node.Build() + if err != nil { + t.Fatalf("Build() = %v", err) + } + if !cmp.Equal(gotBuilt, tc.wantBuilt) { + t.Errorf("Build() = %v, want %v", gotBuilt, tc.wantBuilt) + } + + gotChildren := node.Children() + if !cmp.Equal(gotChildren, tc.wantChildren, + cmp.AllowUnexported(rawCELExprTreeNode{}), + cmp.AllowUnexported(andCELExprTreeNode{}), + cmp.AllowUnexported(orCELExprTreeNode{}), + cmp.AllowUnexported(notCELExprTreeNode{}), + ) { + t.Errorf("Children() = %v, want %v", gotChildren, tc.wantChildren) + } + }) + } +} + +// TestCELExprTreeNode_LogicalNot_Erred tests LogicalNot error scenarios. +func TestCELExprTreeNode_LogicalNot_Erred(t *testing.T) { + testCases := []struct { + name string + child CELExprTreeNode + wantErrSubstring string + }{ + { + name: "nil child", + child: nil, + wantErrSubstring: "child node is nil", + }, + { + name: "child parse error", + child: RawCELExpr(""), + wantErrSubstring: "failed to build child node: raw CEL expression cannot be empty", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + node := LogicalNot(tc.child) + + gotBuilt, err := node.Build() + if err == nil { + t.Fatalf("Build() = nil, want error") + } + if !strings.Contains(err.Error(), tc.wantErrSubstring) { + t.Errorf("Build() error = %v, want error with substring %s", err, tc.wantErrSubstring) + } + if gotBuilt != "" { + t.Errorf("Build() = %v, want empty string on error", gotBuilt) + } + }) + } +} diff --git a/pkg/admissionpolicymanager/commons.go b/pkg/admissionpolicymanager/commons.go index 6a2bc7fe5..59ee76f47 100644 --- a/pkg/admissionpolicymanager/commons.go +++ b/pkg/admissionpolicymanager/commons.go @@ -18,6 +18,7 @@ package admissionpolicymanager import ( "context" + "fmt" "regexp" "strings" @@ -81,3 +82,15 @@ func validateCELStringLiterals(strs ...string) error { } return nil } + +func isInNamespaceWithPrefix(p string) CELExprTreeNode { + return RawCELExpr(fmt.Sprintf(`request.namespace.startsWith("%s")`, p)) +} + +func isFromUsername(u string) CELExprTreeNode { + return RawCELExpr(fmt.Sprintf(`request.userInfo.username == "%s"`, u)) +} + +func isFromUserGroup(g string) CELExprTreeNode { + return RawCELExpr(fmt.Sprintf(`"%s" in request.userInfo.groups`, g)) +} diff --git a/pkg/admissionpolicymanager/manager.go b/pkg/admissionpolicymanager/manager.go index 9d8b0f6c3..ca0305249 100644 --- a/pkg/admissionpolicymanager/manager.go +++ b/pkg/admissionpolicymanager/manager.go @@ -88,7 +88,7 @@ type PolicyWithBindings struct { type ValidatingAdmissionPolicyGenerator interface { Name() string Validate() error - PoliciesWithBindings() []PolicyWithBindings + PoliciesWithBindings() ([]PolicyWithBindings, error) } type PolicyManager struct { @@ -165,7 +165,10 @@ func (m *PolicyManager) createOrUpdatePoliciesAndBindingsForEnabledGenerators(ct return nil, nil, errors.Wraps(err, "policy generator is invalid", "generator", gen.Name()) } - policiesWithBindings := gen.PoliciesWithBindings() + policiesWithBindings, err := gen.PoliciesWithBindings() + if err != nil { + return nil, nil, errors.Wraps(err, "failed to build policies with bindings", "generator", gen.Name()) + } for _, pb := range policiesWithBindings { policy := pb.Policy diff --git a/pkg/admissionpolicymanager/manager_integration_test.go b/pkg/admissionpolicymanager/manager_integration_test.go index 37b1a240b..de818b976 100644 --- a/pkg/admissionpolicymanager/manager_integration_test.go +++ b/pkg/admissionpolicymanager/manager_integration_test.go @@ -111,7 +111,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, Validations: []admissionregistrationv1.Validation{ { - Expression: `request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")`, + Expression: `(request.namespace.startsWith("fleet-")) || (request.namespace.startsWith("kube-"))`, Message: "creating pods and replicas is disallowed in the fleet hub cluster", Reason: ptr.To(metav1.StatusReasonForbidden), }, @@ -166,7 +166,7 @@ var _ = Describe("Policies, Policy Bindings and their Effects", Ordered, func() }, Validations: []admissionregistrationv1.Validation{ { - Expression: `!(request.namespace.startsWith("fleet-") || request.namespace.startsWith("kube-")) || (request.userInfo.username == "system:kube-scheduler" || request.userInfo.username == "system:kube-controller-manager" || "system:nodes" in request.userInfo.groups || "system:masters" in request.userInfo.groups || "kubeadm:cluster-admins" in request.userInfo.groups || "system:serviceaccounts" in request.userInfo.groups)`, + Expression: `(!((request.namespace.startsWith("fleet-")) || (request.namespace.startsWith("kube-")))) || ((request.userInfo.username == "system:kube-scheduler") || (request.userInfo.username == "system:kube-controller-manager") || ("system:nodes" in request.userInfo.groups) || ("system:masters" in request.userInfo.groups) || ("kubeadm:cluster-admins" in request.userInfo.groups) || ("system:serviceaccounts" in request.userInfo.groups))`, Message: "writing service accounts in reserved namespaces or requesting tokens from such service accounts is disallowed", Reason: ptr.To(metav1.StatusReasonForbidden), }, diff --git a/pkg/admissionpolicymanager/podsnreplicasets.go b/pkg/admissionpolicymanager/podsnreplicasets.go index f53dd4c12..9d9245a15 100644 --- a/pkg/admissionpolicymanager/podsnreplicasets.go +++ b/pkg/admissionpolicymanager/podsnreplicasets.go @@ -20,9 +20,6 @@ limitations under the License. package admissionpolicymanager import ( - "fmt" - "strings" - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -73,12 +70,16 @@ func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) Validate() error // replicasets in non-reserved namespaces. // // For simplicity reasons, the code here assumes that the generator has been validated before PoliciesWithBindings() is called. -func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) PoliciesWithBindings() []PolicyWithBindings { - celExprSegs := []string{} +func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) PoliciesWithBindings() ([]PolicyWithBindings, error) { + isInReservedNamespaces := LogicalOr() for _, prefix := range g.ReservedNamespacePrefixes { - celExprSegs = append(celExprSegs, fmt.Sprintf(`request.namespace.startsWith("%s")`, prefix)) + isInReservedNamespaces.Add(isInNamespaceWithPrefix(prefix)) + } + + celExpr, err := isInReservedNamespaces.Build() + if err != nil { + return nil, errors.Wraps(err, "failed to build CEL expression") } - celExpr := strings.Join(celExprSegs, " || ") policy := &admissionregistrationv1.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ @@ -142,5 +143,5 @@ func (g *PodsAndReplicaSetsValidatingAdmissionPolicyGenerator) PoliciesWithBindi Policy: policy, Bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{binding}, }, - } + }, nil } diff --git a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go index 33f1ecc17..35931ecc5 100644 --- a/pkg/admissionpolicymanager/svcaccountsntokenreqs.go +++ b/pkg/admissionpolicymanager/svcaccountsntokenreqs.go @@ -20,9 +20,6 @@ limitations under the License. package admissionpolicymanager import ( - "fmt" - "strings" - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -100,17 +97,7 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Vali // except for requests from certain whitelisted users and user groups. // // For simplicity reasons, the code here assumes that the generator has been validated before PoliciesWithBindings() is called. -func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) PoliciesWithBindings() []PolicyWithBindings { - celExprAccSegs := []string{} - - // Exempt whitelisted users from this admission policy. - for _, username := range g.WhitelistedUsernames { - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, username)) - } - // Exempt whitelisted user groups from this admission policy. - for _, userGroup := range g.WhitelistedUserGroups { - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, userGroup)) - } +func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) PoliciesWithBindings() ([]PolicyWithBindings, error) { // Exempt requests from the Kubernetes scheduler, any of the nodes, and (esp.) the // Kubernetes controller manager from this admission policy. // @@ -118,29 +105,58 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Poli // --use-service-account-credentials=true, creates a service account token for many of its controllers // and uses those tokens to authenticate to the Kubernetes API server. It retrieves a token // via the TokenRequest API; failure to exempt this scenario may lead to critical errors. - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeSchedulerUserName)) - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`request.userInfo.username == "%s"`, kubeControllerManagerUserName)) - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, kubeNodeUserGroup)) + isFromKubeScheduler := isFromUsername(kubeSchedulerUserName) + isFromKubeControllerManager := isFromUsername(kubeControllerManagerUserName) + isFromNodeUserGroup := isFromUserGroup(kubeNodeUserGroup) + // Exempt requests from cluster admin users from this admission policy. - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, adminUserGroup)) + isFromClusterAdmins := isFromUserGroup(adminUserGroup) + // Exempt kubeadm cluster admins from this policy as well, so that bootstrapping a hub cluster with // kubeadm credentials can proceed without being blocked. - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, kubeadmAdminUserGroup)) + isFromKubeadmClusterAdmins := isFromUserGroup(kubeadmAdminUserGroup) + // Exempt service accounts from this admission policy. Note that VAP check happens after authentication and // authorization have been performed. This is added to keep things consistent with the original webhook behavior, // and also for the reason that some controller manager components (e.g., the service account controller) // need to create service accounts as part of their normal operations. - celExprAccSegs = append(celExprAccSegs, fmt.Sprintf(`"%s" in request.userInfo.groups`, svcAccountUserGroup)) + isFromSvcAccounts := isFromUserGroup(svcAccountUserGroup) + + isFromAllowedRequesters := LogicalOr( + isFromKubeScheduler, + isFromKubeControllerManager, + isFromNodeUserGroup, + isFromClusterAdmins, + isFromKubeadmClusterAdmins, + isFromSvcAccounts, + ) + + // Exempt additionally configured users from this admission policy. + for _, username := range g.WhitelistedUsernames { + isFromAllowedRequesters.Add(isFromUsername(username)) + } - celExprAcc := strings.Join(celExprAccSegs, " || ") + // Exempt additionally configured user groups from this admission policy. + for _, userGroup := range g.WhitelistedUserGroups { + isFromAllowedRequesters.Add(isFromUserGroup(userGroup)) + } - celExprNSSegs := []string{} + // Allow the request if it is not targeting reserved namespaces. + isInReservedNamespaces := LogicalOr() for _, prefix := range g.ReservedNamespacePrefixes { - celExprNSSegs = append(celExprNSSegs, fmt.Sprintf(`request.namespace.startsWith("%s")`, prefix)) + isInReservedNamespaces.Add(isInNamespaceWithPrefix(prefix)) } - celExprNS := strings.Join(celExprNSSegs, " || ") - celExpr := fmt.Sprintf("!(%s) || (%s)", celExprNS, celExprAcc) + celExprTree := LogicalOr( + LogicalNot( + isInReservedNamespaces, + ), + isFromAllowedRequesters, + ) + celExpr, err := celExprTree.Build() + if err != nil { + return nil, errors.Wraps(err, "failed to build CEL expression") + } policy := &admissionregistrationv1.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ @@ -209,5 +225,5 @@ func (g *ServiceAccountsAndTokenRequestsValidatingAdmissionPolicyGenerator) Poli Policy: policy, Bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{binding}, }, - } + }, nil } From f83656158cc373f4a722a1b4b203fecf4536a1a5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:08:53 -0500 Subject: [PATCH 2/7] chore: bump golang.org/x/net to v0.55.0 and golang.org/x/sys to v0.45.0 to fix CVEs (#739) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- go.mod | 16 ++++++++-------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index a602263d6..9147972fd 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( go.goms.io/fleet-networking v0.3.3 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.18.0 + golang.org/x/sync v0.20.0 golang.org/x/time v0.11.0 gomodules.xyz/jsonpatch/v2 v2.4.0 k8s.io/api v0.34.1 @@ -38,6 +38,7 @@ require ( sigs.k8s.io/cloud-provider-azure/pkg/azclient v0.5.20 sigs.k8s.io/cluster-inventory-api v0.0.0-20251028164203-2e3fabb46733 sigs.k8s.io/controller-runtime v0.22.4 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -108,14 +109,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -128,7 +129,6 @@ require ( sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index 1801eb133..5a56c7731 100644 --- a/go.sum +++ b/go.sum @@ -326,8 +326,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -336,35 +336,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 3caed866b6b9c04d3532d8bbc65c720adafc8f00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:37:48 -0700 Subject: [PATCH 3/7] chore: bump oss/go/microsoft/golang from 1.25.11 to 1.26.4 in /docker (#738) Bumps oss/go/microsoft/golang from 1.25.11 to 1.26.4. --- updated-dependencies: - dependency-name: oss/go/microsoft/golang dependency-version: 1.26.4 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/hub-agent.Dockerfile | 2 +- docker/member-agent.Dockerfile | 2 +- docker/refresh-token.Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/hub-agent.Dockerfile b/docker/hub-agent.Dockerfile index f0e39f01e..a9e7aa444 100644 --- a/docker/hub-agent.Dockerfile +++ b/docker/hub-agent.Dockerfile @@ -1,5 +1,5 @@ # Build the hubagent binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.25.11 AS builder +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.4 AS builder ARG GOOS=linux ARG GOARCH=amd64 diff --git a/docker/member-agent.Dockerfile b/docker/member-agent.Dockerfile index 529c80495..9fcfa3a92 100644 --- a/docker/member-agent.Dockerfile +++ b/docker/member-agent.Dockerfile @@ -1,5 +1,5 @@ # Build the memberagent binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.25.11 AS builder +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.4 AS builder ARG GOOS=linux ARG GOARCH=amd64 diff --git a/docker/refresh-token.Dockerfile b/docker/refresh-token.Dockerfile index 0945d32f1..d103eda28 100644 --- a/docker/refresh-token.Dockerfile +++ b/docker/refresh-token.Dockerfile @@ -1,5 +1,5 @@ # Build the refreshtoken binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.25.11 AS builder +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.4 AS builder ARG GOOS="linux" ARG GOARCH="amd64" From 5b71aae0a602bcd03a8b672ce43af333169cc4c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:32:32 -0700 Subject: [PATCH 4/7] chore: bump actions/checkout from 4 to 6 (#737) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/chart.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- .github/workflows/code-lint.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/codespell.yml | 2 +- .github/workflows/markdown-lint.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/squad-ci.yml | 2 +- .github/workflows/squad-docs.yml | 2 +- .github/workflows/squad-heartbeat.yml | 2 +- .github/workflows/squad-insider-release.yml | 2 +- .github/workflows/squad-issue-assign.yml | 2 +- .github/workflows/squad-label-enforce.yml | 2 +- .github/workflows/squad-preview.yml | 2 +- .github/workflows/squad-promote.yml | 4 ++-- .github/workflows/squad-release.yml | 2 +- .github/workflows/squad-triage.yml | 2 +- .github/workflows/sync-squad-labels.yml | 2 +- .github/workflows/trivy.yml | 2 +- .github/workflows/upgrade.yml | 6 +++--- 20 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index e5c780fc0..184e60495 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -27,7 +27,7 @@ jobs: needs: export-registry runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: submodules: true fetch-depth: 0 @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Login to GitHub Container Registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 237738f90..ec45f04cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Set up Ginkgo CLI run: | @@ -118,7 +118,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install Ginkgo CLI run: | diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 82caa2f1f..e0f853b36 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -42,7 +42,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: submodules: true @@ -63,7 +63,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: golangci-lint run: make lint @@ -76,7 +76,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Set up Helm uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ae8e07dc4..b03cef53b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 8a710484b..f5d349197 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.7 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.1.7 - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # master with: check_filenames: true diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml index 2d96457c5..0fba45539 100644 --- a/.github/workflows/markdown-lint.yml +++ b/.github/workflows/markdown-lint.yml @@ -10,7 +10,7 @@ jobs: markdown-link-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1 with: # this will only show errors in the output diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7db4cebea..3426579a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.export-registry.outputs.tag }} diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml index c34a2ec72..923e97d8d 100644 --- a/.github/workflows/squad-ci.yml +++ b/.github/workflows/squad-ci.yml @@ -15,7 +15,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build and test run: | diff --git a/.github/workflows/squad-docs.yml b/.github/workflows/squad-docs.yml index 1c6b7ac9a..21e80ae2b 100644 --- a/.github/workflows/squad-docs.yml +++ b/.github/workflows/squad-docs.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build docs run: | diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml index 1b75fda3e..edc1eac28 100644 --- a/.github/workflows/squad-heartbeat.yml +++ b/.github/workflows/squad-heartbeat.yml @@ -25,7 +25,7 @@ jobs: heartbeat: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check triage script id: check-script diff --git a/.github/workflows/squad-insider-release.yml b/.github/workflows/squad-insider-release.yml index c2e551f17..c54d0f2e9 100644 --- a/.github/workflows/squad-insider-release.yml +++ b/.github/workflows/squad-insider-release.yml @@ -12,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/squad-issue-assign.yml b/.github/workflows/squad-issue-assign.yml index ad140f42d..0137b6d4e 100644 --- a/.github/workflows/squad-issue-assign.yml +++ b/.github/workflows/squad-issue-assign.yml @@ -14,7 +14,7 @@ jobs: if: startsWith(github.event.label.name, 'squad:') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Identify assigned member and trigger work uses: actions/github-script@v7 diff --git a/.github/workflows/squad-label-enforce.yml b/.github/workflows/squad-label-enforce.yml index 633d220df..df6ef8a03 100644 --- a/.github/workflows/squad-label-enforce.yml +++ b/.github/workflows/squad-label-enforce.yml @@ -12,7 +12,7 @@ jobs: enforce: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enforce mutual exclusivity uses: actions/github-script@v7 diff --git a/.github/workflows/squad-preview.yml b/.github/workflows/squad-preview.yml index bef1f8641..11465a651 100644 --- a/.github/workflows/squad-preview.yml +++ b/.github/workflows/squad-preview.yml @@ -12,7 +12,7 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Build and test run: | diff --git a/.github/workflows/squad-promote.yml b/.github/workflows/squad-promote.yml index 9d315b1d1..2406d51b4 100644 --- a/.github/workflows/squad-promote.yml +++ b/.github/workflows/squad-promote.yml @@ -18,7 +18,7 @@ jobs: name: Promote dev → preview runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -70,7 +70,7 @@ jobs: needs: dev-to-preview runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/squad-release.yml b/.github/workflows/squad-release.yml index c796cd2b7..069ba384e 100644 --- a/.github/workflows/squad-release.yml +++ b/.github/workflows/squad-release.yml @@ -12,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/squad-triage.yml b/.github/workflows/squad-triage.yml index d118a2813..ad50639c6 100644 --- a/.github/workflows/squad-triage.yml +++ b/.github/workflows/squad-triage.yml @@ -13,7 +13,7 @@ jobs: if: github.event.label.name == 'squad' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Triage issue via Lead agent uses: actions/github-script@v7 diff --git a/.github/workflows/sync-squad-labels.yml b/.github/workflows/sync-squad-labels.yml index 699fc680f..999bed8ef 100644 --- a/.github/workflows/sync-squad-labels.yml +++ b/.github/workflows/sync-squad-labels.yml @@ -15,7 +15,7 @@ jobs: sync-labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Parse roster and sync labels uses: actions/github-script@v7 diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 09d72b32d..b669bd5e6 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -44,7 +44,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Login to ${{ env.REGISTRY }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade.yml index 2c6c0ae2f..ea0a4f8b4 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade.yml @@ -44,7 +44,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. @@ -127,7 +127,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. @@ -210,7 +210,7 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. From fdb3aacca6678e85ef9884a2c278618319d2f335 Mon Sep 17 00:00:00 2001 From: Yetkin Timocin Date: Fri, 26 Jun 2026 09:58:36 -0700 Subject: [PATCH 5/7] ci: prepare release pipeline for -rc tags and add autogen release notes (#713) * ci: prepare release pipeline for -rc tags and add autogen release notes Foundational hygiene + workflow correctness for the release revamp tracked in kubefleet-dev/kubefleet#693. Four mechanical changes that together unblock cutting the first v0.4.0-rc.1 tag. 1. Anchor the SemVer regex and accept -rc.N in setup-release.yml. Closes the partial-match bug (e.g. v0.4.0extrastuff previously validated) and adds support for v*.*.*-rc.N pre-release tags following the Kubernetes convention. 2. Exclude -rc* tags from upgrade-compat tag discovery in upgrade.yml. Without this, the first RC becomes the "previous release" for all three compat jobs and the suite fails. Uses git rev-list and git describe --exclude= globs. 3. Add .github/release.yml for GitHub auto-generated release notes, with label-based categorization that mirrors the project's existing PR-title prefixes. Required release-note/* labels will be created out-of-band; see the PR description. 4. Delete charts/{hub,member}-agent/crdbases/ - vestigial copies of config/crd/bases/, unreferenced by chart templates, Makefile, or any shell script. Maps to kubefleet-dev/kubefleet#693 Phase 1 bullets 1 (partial), 6, 7. Signed-off-by: Yetkin Timocin * docs: document PR title prefixes and release-note labels Adds two sections to CONTRIBUTING.md, completing the auto-generated release-notes setup from the same PR: - "Pull request titles" enumerates the prefixes enforced by pr-title-lint.yml and points contributors at make reviewable, which the PR template already mandates but CONTRIBUTING.md did not surface. - "Release note labels" maps each title prefix to its release-note/* label and documents the additive labels (breaking, security, none) and the distinction between release-note/none (entry suppressed, PR still in dataset) and ignore-for-release (PR excluded entirely). Signed-off-by: Yetkin Timocin * ci: serialize release workflows and harden imagetools loop Two small additions in the same release-correctness theme as the rest of this PR: - Add concurrency groups to release.yml (per-ref) and chart.yml (global). Both use cancel-in-progress: false - aborting an in-flight image or chart push is worse than letting it finish. release-images is keyed on github.ref so different tags don't serialize against each other; the chart group is global because helm-gh-pages always rewrites the gh-pages branch and concurrent runs would race. - Add set -euo pipefail to the "Tag and push images without v prefix" step in release.yml. Without it, an imagetools failure on image 1 silently lets the loop continue for images 2-3, leaving the release partially retagged. The matching steps in chart.yml already use strict mode. Signed-off-by: Yetkin Timocin * docs: propose support window and CVE response SLO in SECURITY.md Draft for maintainer discussion on this PR. The values pick a coherent trio that the release-process revamp in #693 has otherwise been blocked on (Q3 in the epic): - Support window: N/N-1. Only the latest minor and the immediately preceding minor receive security patches. At the proposed ~3-month cadence this is ~6 months effective patch coverage per minor. - Response SLO: 14 days for Critical (CVSS 9.0+), 45 days for High (7.0-8.9), best-effort for Medium/Low. Marked aspirational; revisit after one quarterly cycle. - Pre-1.0 caveat called out explicitly: "supported" applies to security backports, not API stability. Reviewers: please push back on any of these on the PR thread - the values are deliberately concrete to force the conversation, not because they're settled. Signed-off-by: Yetkin Timocin * ci: address review feedback on release-workflow gates and globs Three follow-ups from the multi-agent review of this PR: - chart.yml: filter pre-release tags via a negative pattern in the push.tags trigger. Without this, v0.4.0-rc.1 would publish an RC chart into the stable gh-pages index (helm-gh-pages appends to index.yaml), making it visible to every "helm repo add" consumer. The negative pattern form is the GitHub-idiomatic way to combine include/exclude for tags. (Blocking finding #1.) - chart.yml: scope the helm-chart-publish concurrency to the publish-github-pages job only. The OCI publish pushes immutable per-tag blobs and is safe to run in parallel across tags; serializing it added latency with no correctness benefit. (Should-fix #4.) - upgrade.yml: tighten the upgrade-test tag-exclusion glob from '*-rc*' to '*-rc.*' so it only matches the project's dot-separated RC tag form and not hypothetical refs like 'release-rc-branch'. Both git rev-list --exclude and git describe --exclude get the tightened pattern. (Should-fix #9.) - release.yml: add set -euo pipefail to the "Verify images" step for consistency with the imagetools step. Pure echo loop today so harmless, but the precedent prevents future bugs. (Consider.) Signed-off-by: Yetkin Timocin * ci: scope short-tag publish to stable releases and tighten chart glob Two follow-ups from the second round of multi-agent review on this PR: - release.yml: gate the "Tag and push images without v prefix" step with if: !contains(needs.export-registry.outputs.tag, '-rc.'). RC tags continue to build and push images under the long form (e.g. :v0.4.0-rc.1) so testers can pull them, but we no longer alias them into the short-tag namespace (:0.4.0-rc.1). That namespace is reserved for stable releases that consumers pin to. Also refactor the Verify images step into a per-image loop with a matching conditional so the output reflects what was actually published. - chart.yml: tighten the negative pattern from "!*-rc.*" to "!v*-rc.*". Scopes the negation to the project's own v-prefixed tag form; hypothetical non-v-prefixed tags containing -rc. (from other tooling) would otherwise be silently dropped. Signed-off-by: Yetkin Timocin * docs: address review feedback on CONTRIBUTING.md and SECURITY.md Bundles the response to the multi-agent review of this PR. CONTRIBUTING and SECURITY are touched together because the doc-review findings were interrelated and the fixes land cleanly as one batch. CONTRIBUTING.md: - Add release-note/* mappings for style:, interface:, util:, revert:. style/interface/util fold into release-note/chore; revert folds into release-note/fix (operationally a fix from the user's perspective; confirmed against the project's historical revert PR #454). - Fix the contradiction between "carry exactly one" and the additive labels by switching to "one base ... additive labels may be stacked". - Expand the release-note/breaking threshold from "any change that breaks an API or behavior" to: requires user action (manifest edit, CRD/RBAC reapply, webhook config update, member-cluster re-join) OR alters scheduling/override/apply semantics that re-rank or re-apply existing placements without a manifest change. Pre-1.0 internal refactors of v1alpha1 shapes that don't require migration steps or change behavior do not qualify. - Sharpen the release-note/none vs ignore-for-release distinction: none keeps the PR visible in GitHub's auto-notes drafter UI; ignore-for-release hides it entirely - "default to this for CI-only or internal-cleanup PRs". SECURITY.md: - Add TODO HTML comment at the top scoping the three new sections to kubefleet-dev/kubefleet#693 Q3 + the discussion on this PR. - Switch "follows N/N-1" to "targets N/N-1". Replace the apologetic "best-effort basis" caveat with a factual statement that the project has held a roughly quarterly cadence since v0.1.0 (giving ~6 months effective patch coverage) with possible minor slippage while pre-1.0. - Add an upgrade-path sentence directing users on EOL minors to follow the project's upgrade documentation. - Add a new "Coordinated disclosure" section covering embargo window (TBD, with a floor: reporters notified before disclosure), vendor advance notification (TBD, references the CNCF TAG-Security template for the conventional distributors-list form), and GitHub private vulnerability reporting (to be enabled). All values in the new SECURITY.md sections remain proposals for the maintainer thread on this PR; the TODO comment makes that explicit. Signed-off-by: Yetkin Timocin * docs: correct cadence claim and generalize alpha/beta carve-out Two final wording fixes from the third round of multi-agent review on this PR. Both content reviewers flagged the cadence wording as factually inaccurate; one also flagged the alpha-version carve-out as too narrow given that v1beta1 types already exist in the codebase. - SECURITY.md: replace "roughly quarterly release cadence since v0.1.0 ... approximately six months of patch coverage" with the actual observed cadence. Real minor-release intervals: v0.0.1->v0.1 = 6.7 months (pre-sandbox outlier), v0.1->v0.2 = 2.1 months, v0.2->v0.3.0 = 2.5 months. The supported text now reads "roughly 2-3 month cadence since v0.2, giving approximately four to six months of patch coverage" which matches the data. - CONTRIBUTING.md: generalize the breaking-label carve-out from "internal refactors that touch v1alpha1 shapes" to "internal refactors of any alpha or beta API shape." KubeFleet already ships v1beta1 types (placement/v1beta1 is the current stable line), and a literal reading of the v1alpha1-only wording could mislead a future reviewer into thinking v1beta1 refactors automatically qualify as breaking. Also tighten "change semantics" to "change observable semantics" since unobservable internal-state changes are not user-breaking. Signed-off-by: Yetkin Timocin --------- Signed-off-by: Yetkin Timocin --- .github/release.yml | 41 +++++++++++++ .github/workflows/chart.yml | 12 ++++ .github/workflows/release.yml | 29 ++++++++-- .github/workflows/setup-release.yml | 4 +- .github/workflows/upgrade.yml | 6 +- CONTRIBUTING.md | 30 ++++++++++ SECURITY.md | 58 +++++++++++++++++++ ...netes-fleet.io_internalmemberclusters.yaml | 1 - ...er.kubernetes-fleet.io_memberclusters.yaml | 1 - ...es-fleet.io_clusterresourceplacements.yaml | 1 - .../placement.kubernetes-fleet.io_works.yaml | 1 - ...ment.kubernetes-fleet.io_appliedworks.yaml | 1 - 12 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 .github/release.yml delete mode 120000 charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml delete mode 120000 charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_memberclusters.yaml delete mode 120000 charts/hub-agent/crdbases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml delete mode 120000 charts/hub-agent/crdbases/placement.kubernetes-fleet.io_works.yaml delete mode 120000 charts/member-agent/crdbases/placement.kubernetes-fleet.io_appliedworks.yaml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..0d24961bf --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,41 @@ +# Configures GitHub auto-generated release notes. +# https://docs.github.com/repositories/releasing-projects-on-github/automatically-generated-release-notes +# +# Categorization is label-based. Contributors should add a single release-note/* +# label to each PR; the label should match the PR title prefix +# (e.g. feat: -> release-note/feature, fix: -> release-note/fix). +# PRs with no release-note label fall into "Other Changes". +changelog: + exclude: + labels: + - ignore-for-release + - release-note/none + categories: + - title: Breaking Changes + labels: + - release-note/breaking + - title: Security + labels: + - release-note/security + - title: New Features + labels: + - release-note/feature + - title: Bug Fixes + labels: + - release-note/fix + - title: Documentation + labels: + - release-note/docs + - title: Tests + labels: + - release-note/test + - title: Refactors + labels: + - release-note/refactor + - title: Maintenance and Dependencies + labels: + - release-note/chore + - dependencies + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index 184e60495..4f285b52b 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -2,8 +2,13 @@ name: Helm Chart Publisher on: push: + # Pre-release tags (e.g. v0.4.0-rc.1) build images via release.yml but + # must not land in the public Helm index. The negative pattern below + # filters them out; workflow_dispatch can still publish a specific + # tag manually if ever needed. tags: - "v*.*.*" + - "!v*-rc.*" workflow_dispatch: inputs: tag: @@ -26,6 +31,13 @@ jobs: publish-github-pages: needs: export-registry runs-on: ubuntu-latest + # Only the gh-pages publish needs serialization: helm-gh-pages always + # rewrites the gh-pages branch, so concurrent runs for different tags + # would race. The OCI publish below pushes immutable per-tag blobs and + # is safe to run in parallel across tags, so it stays unguarded. + concurrency: + group: helm-chart-publish-gh-pages + cancel-in-progress: false steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3426579a4..cd499a31c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,14 @@ permissions: contents: read packages: write +# Serialize releases per ref so concurrent tag pushes can't race on image +# pushes to the same ${REGISTRY}/${IMAGE}:${TAG}. Different tags can still +# run in parallel. We never want cancel-in-progress here: aborting a +# half-pushed image is worse than letting it finish. +concurrency: + group: release-images-${{ github.ref }} + cancel-in-progress: false + env: REGISTRY: ghcr.io HUB_AGENT_IMAGE_NAME: hub-agent @@ -56,10 +64,18 @@ jobs: run: | make push + # The short-tag (e.g. ":0.4.0") aliases the long form for stable releases + # only. RC images are published under the long form (":v0.4.0-rc.1") for + # testers, but we deliberately do NOT alias them to a short tag - the + # short-tag namespace is reserved for stable releases that consumers can + # safely pin to (and "imagetools create" with an RC alias would publish + # "0.4.0-rc.1" into that namespace, muddying it). - name: Tag and push images without v prefix + if: ${{ !contains(needs.export-registry.outputs.tag, '-rc.') }} env: VERSION: ${{ needs.export-registry.outputs.version }} run: | + set -euo pipefail for IMAGE in ${{ env.HUB_AGENT_IMAGE_NAME }} ${{ env.MEMBER_AGENT_IMAGE_NAME }} ${{ env.REFRESH_TOKEN_IMAGE_NAME }}; do docker buildx imagetools create \ --tag "${{ env.REGISTRY }}/${IMAGE}:${VERSION}" \ @@ -70,10 +86,11 @@ jobs: env: VERSION: ${{ needs.export-registry.outputs.version }} run: | + set -euo pipefail echo "✅ Published images:" - echo " - ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${{ env.TAG }}" - echo " - ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${VERSION}" - echo " - ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${{ env.TAG }}" - echo " - ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${VERSION}" - echo " - ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${{ env.TAG }}" - echo " - ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${VERSION}" + for IMAGE in ${{ env.HUB_AGENT_IMAGE_NAME }} ${{ env.MEMBER_AGENT_IMAGE_NAME }} ${{ env.REFRESH_TOKEN_IMAGE_NAME }}; do + echo " - ${{ env.REGISTRY }}/${IMAGE}:${{ env.TAG }}" + if [[ "${{ env.TAG }}" != *-rc.* ]]; then + echo " - ${{ env.REGISTRY }}/${IMAGE}:${VERSION}" + fi + done diff --git a/.github/workflows/setup-release.yml b/.github/workflows/setup-release.yml index 685050d71..dfa0a3ef8 100644 --- a/.github/workflows/setup-release.yml +++ b/.github/workflows/setup-release.yml @@ -32,8 +32,8 @@ jobs: - id: setup run: | TAG="${{ inputs.tag }}" - if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then - echo "Error: Invalid release tag '${TAG}'. Expected format: v*.*.*" + if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "Error: Invalid release tag '${TAG}'. Expected format: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc.N" exit 1 fi diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade.yml index ea0a4f8b4..456d648bd 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade.yml @@ -69,7 +69,7 @@ jobs: echo "Fetch all tags..." git fetch --all - GIT_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) + GIT_TAG=$(git describe --tags --exclude='*-rc.*' $(git rev-list --tags --exclude='*-rc.*' --max-count=1)) else echo "A tag is specified; go back to the state tracked by the specified tag." echo "Fetch all tags..." @@ -152,7 +152,7 @@ jobs: echo "Fetch all tags..." git fetch --all - GIT_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) + GIT_TAG=$(git describe --tags --exclude='*-rc.*' $(git rev-list --tags --exclude='*-rc.*' --max-count=1)) else echo "A tag is specified; go back to the state tracked by the specified tag." echo "Fetch all tags..." @@ -235,7 +235,7 @@ jobs: echo "Fetch all tags..." git fetch --all - GIT_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) + GIT_TAG=$(git describe --tags --exclude='*-rc.*' $(git rev-list --tags --exclude='*-rc.*' --max-count=1)) else echo "A tag is specified; go back to the state tracked by the specified tag." echo "Fetch all tags..." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68416e842..0e8ba8dd4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,33 @@ The KubeFleet project has adopted the CNCF Code of Conduct. Refer to our [Commun ## Issue and pull request management Anyone can comment on issues and submit reviews for pull requests. In order to be assigned an issue or pull request, you can leave a `/assign ` comment on the issue or pull request. + +## Pull request titles + +PR titles must begin with one of the following prefixes (enforced by [`pr-title-lint.yml`](.github/workflows/pr-title-lint.yml)): + +`feat:`, `fix:`, `docs:`, `test:`, `style:`, `interface:`, `util:`, `chore:`, `ci:`, `perf:`, `refactor:`, `revert:` + +Add `make reviewable` to your workflow before opening a PR — the PR template will remind you, but running it locally first saves a round trip. + +## Release note labels + +Each PR should carry one base `release-note/*` label matching its title prefix; additive labels (below) may be stacked on top. + +| PR title prefix | Label | +| --- | --- | +| `feat:` / `perf:` | `release-note/feature` | +| `fix:` / `revert:` | `release-note/fix` | +| `docs:` | `release-note/docs` | +| `test:` | `release-note/test` | +| `chore:` / `ci:` / `style:` / `interface:` / `util:` | `release-note/chore` | +| `refactor:` | `release-note/refactor` | + +Additive labels (stack on top of the above when applicable): + +- `release-note/breaking` — a change that requires user action to upgrade (manifest edit, CRD reapply, RBAC reapply, webhook config update, member-cluster re-join, etc.) **or** that alters scheduling, override, or apply semantics in a way that re-ranks or re-applies existing placements without a manifest change. Pre-1.0, internal refactors of any alpha or beta API shape that don't require migration steps or change observable semantics do not qualify. +- `release-note/security` — security fixes or vulnerability disclosures +- `release-note/none` — suppresses the entry in release notes but keeps the PR visible in GitHub's auto-notes drafter UI +- `ignore-for-release` — hides the PR entirely from auto-generated notes. Default to this for CI-only or internal-cleanup PRs with no user impact. + +PRs with no `release-note/*` label fall into "Other Changes" in the generated notes. Dependabot PRs are labeled `dependencies` automatically and land under "Maintenance and Dependencies" without a `release-note/*` label. diff --git a/SECURITY.md b/SECURITY.md index 50262c4af..f00647786 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,11 +1,69 @@ # Security + + The KubeFleet maintainers takes the security of the project very seriously; we greatly welcomes and appreciates any responsible disclosures of security vulnerabilities. If you believe you have found a security vulnerability in the repository, please follow the steps below to report it to the KubeFleet team. +## Supported versions + +KubeFleet is pre-1.0 and *targets* an `N`/`N-1` support window: the latest minor release and +the one immediately preceding it receive security patches. The project has maintained a roughly +2–3 month minor-release cadence since `v0.2`, giving approximately four to six months of patch +coverage from the GA of any given minor. Minor cadence slippage is possible while we are pre-1.0. + +| Version | Supported | +| --- | --- | +| Latest minor (e.g. `v0.Y.x`) | Yes | +| Previous minor (e.g. `v0.Y-1.x`) | Yes | +| Older minors | No | + +"Supported" here refers to security patch backports only. As a pre-1.0 project, KubeFleet does +not guarantee API stability across minor releases. Users on unsupported minors should upgrade to +a supported minor following the project's upgrade documentation; patches are not backported to +EOL releases. + +## Response SLO + +We commit to the following response targets, measured from the time a report is acknowledged by +the maintainers to the time a patched release is published across all supported minors: + +| Severity (CVSS v3.1) | Target time-to-patch | +| --- | --- | +| Critical (9.0+) | 14 days | +| High (7.0–8.9) | 45 days | +| Medium / Low | Best-effort, no committed SLO | + +These targets are aspirational while we ramp up to consistent release cadence; we will revisit +them after one full quarterly cycle. + +## Coordinated disclosure + +KubeFleet follows responsible-disclosure norms but the operational specifics below are still +being finalized: + +- **Embargo window: TBD.** We have not yet committed to a fixed number of days between + vulnerability acknowledgement and public disclosure. At minimum, reporters will be notified + before public disclosure. The intent is to follow standard CNCF coordinated disclosure + practice (typically 1–7 days for downstream coordination). +- **Vendor advance notification: TBD.** Projects with downstream consumers commonly operate + a distributors mailing list for embargo coordination with packagers and downstream forks + (see the [CNCF TAG-Security `SECURITY.md` template](https://github.com/cncf/tag-security/blob/main/project-resources/templates/SECURITY.md) + for the conventional `cncf--distributors-announce@lists.cncf.io` form). Whether + KubeFleet stands one up depends on demonstrated downstream demand. +- **GitHub private vulnerability reporting:** to be enabled on this repository as the + preferred reporting channel; the maintainer mailing list (see below) remains the fallback + until it is. + +This section will be updated as each item is decided. + ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** Instead, diff --git a/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml b/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml deleted file mode 120000 index 81343bc3f..000000000 --- a/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../config/crd/bases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml \ No newline at end of file diff --git a/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_memberclusters.yaml b/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_memberclusters.yaml deleted file mode 120000 index 88640f17a..000000000 --- a/charts/hub-agent/crdbases/cluster.kubernetes-fleet.io_memberclusters.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../config/crd/bases/cluster.kubernetes-fleet.io_memberclusters.yaml \ No newline at end of file diff --git a/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml b/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml deleted file mode 120000 index 73d196c43..000000000 --- a/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml \ No newline at end of file diff --git a/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_works.yaml b/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_works.yaml deleted file mode 120000 index 1dd97bf95..000000000 --- a/charts/hub-agent/crdbases/placement.kubernetes-fleet.io_works.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../config/crd/bases/placement.kubernetes-fleet.io_works.yaml \ No newline at end of file diff --git a/charts/member-agent/crdbases/placement.kubernetes-fleet.io_appliedworks.yaml b/charts/member-agent/crdbases/placement.kubernetes-fleet.io_appliedworks.yaml deleted file mode 120000 index 9ee31742e..000000000 --- a/charts/member-agent/crdbases/placement.kubernetes-fleet.io_appliedworks.yaml +++ /dev/null @@ -1 +0,0 @@ -../../../config/crd/bases/placement.kubernetes-fleet.io_appliedworks.yaml \ No newline at end of file From 1f6fae9afdba2b48968c22384e5b5bdacceef88c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:53:00 -0500 Subject: [PATCH 6/7] chore: upgrade golang.org/x/crypto to v0.52.0 (#744) * Initial plan * chore: upgrade golang.org/x/crypto to v0.52.0 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9147972fd..91b380fc1 100644 --- a/go.mod +++ b/go.mod @@ -109,7 +109,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect diff --git a/go.sum b/go.sum index 5a56c7731..14f6a2a88 100644 --- a/go.sum +++ b/go.sum @@ -326,8 +326,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= From 6df264cbec1148d0af9d58ee7b68fee274c17f4b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:59:28 -0700 Subject: [PATCH 7/7] ci: bump pinned Ginkgo CLI to v2.23.4 to match go.mod (#696) ci: bump Ginkgo CLI version to v2.23.4 to match go.mod Agent-Logs-Url: https://github.com/kubefleet-dev/kubefleet/sessions/ee40189d-043f-4727-ad2e-239b05bbbc09 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ytimocin <5220939+ytimocin@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/upgrade.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec45f04cb..04d38d4d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Set up Ginkgo CLI run: | - go install github.com/onsi/ginkgo/v2/ginkgo@v2.19.1 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 - name: Prepare necessary environment variables run: | diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade.yml index 456d648bd..4bfba6368 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade.yml @@ -52,7 +52,7 @@ jobs: - name: Set up Ginkgo CLI run: | - go install github.com/onsi/ginkgo/v2/ginkgo@v2.19.1 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 - name: Travel back in time to the before upgrade version run: | @@ -135,7 +135,7 @@ jobs: - name: Set up Ginkgo CLI run: | - go install github.com/onsi/ginkgo/v2/ginkgo@v2.19.1 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 - name: Travel back in time to the before upgrade version run: | @@ -218,7 +218,7 @@ jobs: - name: Set up Ginkgo CLI run: | - go install github.com/onsi/ginkgo/v2/ginkgo@v2.19.1 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4 - name: Travel back in time to the before upgrade version run: |