Skip to content

Commit acc1f8b

Browse files
committed
Addressing review comments part 4:
* Add docs.go and interface docs * Remove pkg/metadata/finalizer.go changes and create a separate PR kubernetes-sigs#625 * nits and adding comments * move envtest makefile changes to kubernetes-sigs#636 * removing labeller changes and moving them to kubernetes-sigs#631 * Fix regression when removing labeller changes: `dynamic-controller Error syncing item, requeuing with rate limit {"item": {"NamespacedKey":"chainsaw-special-quail/test-instance","GVR":{"Group":"kro.run","Version":"v1alpha1","Resource":"checkinstancecreationsimpledeployments"}}, "error": "failed to setup instance: failed to set finalizer: error getting finalizers: .metadata.finalizers accessor error: [] is of the type []string, expected []interface{}"}`
1 parent 334cdfd commit acc1f8b

File tree

15 files changed

+234
-90
lines changed

15 files changed

+234
-90
lines changed

Makefile

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ vet: ## Run go vet against code.
9494
go vet ./...
9595

9696
.PHONY: test
97-
test: manifests generate fmt vet envtest ## Run tests. Use WHAT=unit or WHAT=integration
97+
test: manifests generate fmt vet ## Run tests. Use WHAT=unit or WHAT=integration
9898
ifeq ($(WHAT),integration)
99-
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v ./test/integration/suites/... -coverprofile integration-cover.out
99+
go test -v ./test/integration/suites/... -coverprofile integration-cover.out
100100
else ifeq ($(WHAT),unit)
101101
go test -v ./pkg/... -coverprofile unit-cover.out
102102
else
@@ -190,7 +190,6 @@ $(CHAINSAW): $(LOCALBIN)
190190
fi
191191
test -s $(LOCALBIN)/chainsaw || GOBIN=$(LOCALBIN) GO111MODULE=on go install github.com/kyverno/chainsaw@$(CHAINSAW_VERSION)
192192

193-
ENVTEST_VERSION ?= 1.31.x
194193

195194
.PHONY: ko
196195
ko: $(KO) ## Download ko locally if necessary. If wrong version is installed, it will be removed before downloading.
@@ -216,12 +215,6 @@ $(CONTROLLER_GEN): $(LOCALBIN)
216215
test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
217216
GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
218217

219-
.PHONY: envtest
220-
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
221-
$(ENVTEST): $(LOCALBIN)
222-
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) $(GOPREFIX) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
223-
224-
225218
.PHONY: image
226219
build-image: ko ## Build the kro controller images using ko build
227220
echo "Building kro image $(RELEASE_VERSION).."

pkg/applyset/applyset.go

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,120 @@ func (t ToolingID) String() string {
4949
return fmt.Sprintf("%s/%s", t.Name, t.Version)
5050
}
5151

52+
/*
53+
The Set interface provides methods for:
54+
- Add - Add an object to the set
55+
- Apply - Apply objects in the set to the cluster along with pruning
56+
- DryRun - Dry run calls the kubernetes API with dryrun flag set to true.
57+
No actual resources are created or pruned.
58+
59+
Add() is used to add object to the apply Set.
60+
It takes unstructured object.
61+
It does a get from the cluster to note the resource-version before apply.
62+
63+
Apply() method applies the objects in the set to a Kubernetes cluster. If
64+
the prune parameter is true, any objects that were previously applied but are
65+
no longer in the set will be deleted from the cluster.
66+
67+
DryRun() method can be used to see what changes would be made without actually making them.
68+
69+
Example Usage:
70+
71+
// Create an ApplySet
72+
// aset, err := applyset.New(parent, restMapper, dynamicClient, applySetConfig)
73+
74+
// Add a ConfigMap to the ApplySet
75+
// configMap := &unstructured.Unstructured{ ... }
76+
err = aset.Add(context.TODO(), applyset.ApplyableObject{
77+
Unstructured: configMap,
78+
ID: "my-config-map", // optional
79+
})
80+
if err != nil {
81+
log.Fatalf("Failed to add object to ApplySet: %v", err)
82+
}
83+
84+
// Apply the changes to the cluster (or dry-run)
85+
// To apply:
86+
result, err := aset.Apply(context.TODO(), true) // true to enable pruning
87+
// or dry-run:
88+
// result, err := aset.DryRun(context.TODO(), true)
89+
if err != nil {
90+
log.Fatalf("Failed to apply/dry-run ApplySet: %v", err)
91+
}
92+
93+
if result.Errors() != nil {
94+
fmt.Printf("ApplySet completed with errors: %v\n", result.Errors())
95+
} else {
96+
fmt.Println("ApplySet completed successfully (or dry-run successful).")
97+
}
98+
*/
5299
type Set interface {
53100
Add(ctx context.Context, obj ApplyableObject) error
54101
Apply(ctx context.Context, prune bool) (*ApplyResult, error)
55102
DryRun(ctx context.Context, prune bool) (*ApplyResult, error)
56103
}
57104

58105
type Config struct {
59-
ToolLabels map[string]string
106+
// ToolLabels can be used to inject labels into all resources managed by applyset
107+
ToolLabels map[string]string
108+
109+
// Provide an identifier which is used as field manager for server side apply
110+
// https://kubernetes.io/docs/reference/using-api/server-side-apply/#managers
60111
FieldManager string
61-
ToolingID ToolingID
62-
Log logr.Logger
112+
113+
// concats the name and version and adds it as an annotatiuon
114+
// https://kubernetes.io/docs/reference/using-api/server-side-apply/#managers
115+
ToolingID ToolingID
116+
117+
// Log is used to inject the calling reconciler's logger
118+
Log logr.Logger
63119

64120
// Callbacks
65-
LoadCallback func(obj ApplyableObject, clusterValue *unstructured.Unstructured) error
66-
AfterApplyCallback func(obj AppliedObject) error
121+
122+
// LoadCallback is called after an object is read from the cluster.
123+
// This is done as part of Add() interface call.
124+
LoadCallback func(obj ApplyableObject, clusterValue *unstructured.Unstructured) error
125+
126+
// AfterApplyCallback is called after an object has been applied to the cluster.
127+
// This callback is passed the return value of the apply call.
128+
AfterApplyCallback func(obj AppliedObject) error
129+
130+
// BeforePruneCallback is called before an object is pruned from the cluster.
131+
// This callback is passed the object being pruned.
132+
// This would not be part of the current applyset by definition.
67133
BeforePruneCallback func(obj PrunedObject) error
68134
}
69135

70-
// New creates a new ApplySet
71-
// parent object is expected to be the current one existing in the cluster
136+
/*
137+
New creates a new ApplySet
138+
parent object is expected to be the current one existing in the cluster
139+
Use New() to create an apply Set. This function takes the parent object,
140+
a RESTMapper, a dynamic client, and a configuration object. The parent
141+
object is the object that "owns" the set of resources being applied.
142+
143+
Example usage:
144+
145+
// Set up Kubernetes client and RESTMapper
146+
// dynamicClient = ...
147+
// restMapper = ...
148+
149+
// Define a parent. The parent should exist in the cluster
150+
// Can be any Kubernetes resource even a custom resource instance
151+
parent := &unstructured.Unstructured{ Object: map[string]interface{}{} } }
152+
parent.SetGroupVersionKind(schema.GroupVersionKind{Group: "example.com", Version: "v1", Kind: "MyCustomResource"})
153+
parent.SetName("somename")
154+
155+
// ApplySet configuration
156+
applySetConfig := applyset.Config{
157+
ToolingID: applyset.ToolingID{Name: "my-controller", Version: "v1.0.0"},
158+
FieldManager: "my-controller",
159+
Log: logr.Discard(), // Use a real logger in production
160+
}
161+
162+
// Create an ApplySet
163+
aset, err := applyset.New(parent, restMapper, dynamicClient, applySetConfig)
164+
// if err != nil { ... }
165+
*/
72166
func New(
73167
parent *unstructured.Unstructured,
74168
restMapper meta.RESTMapper,

pkg/applyset/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Copyright 2023 The Kubernetes Authors.
12
// Copyright 2025 The Kube Resource Orchestrator Authors
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");

pkg/applyset/docs.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2023 The Kubernetes Authors.
2+
// Copyright 2025 The Kube Resource Orchestrator Authors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
/*
17+
Package applyset provides a library for managing sets of Kubernetes objects.
18+
The core of this package is the Set interface, which defines the operations
19+
for an apply set. An apply set is a collection of Kubernetes objects that are
20+
managed together as a group. This is useful for managing the resources of a
21+
composite object, like a CRD that creates other resources.
22+
23+
This is implemented as per the KEP:
24+
https://github.com/kubernetes/enhancements/blob/master/keps/sig-cli/3659-kubectl-apply-prune/README.md
25+
26+
Existing implementations of the KEP:
27+
28+
1. kubectl implements the KEP in its codebase:
29+
https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/apply/applyset.go
30+
https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/apply/applyset_pruner.go
31+
cli focussed, pulls in a lot of dependencies not required for a controller.
32+
33+
2. kubernetes-declarative-pattern (kdp)
34+
https://pkg.go.dev/sigs.k8s.io/kubebuilder-declarative-pattern/applylib/applyset
35+
controller focussed, missing callbacks, lifecycle support etc.
36+
37+
Why Fork:
38+
* The kubectl implementation is built for cli and has many dependencies that we dont need in a controller.
39+
* The kdp implementation is good for controller but lacks some enhancements that are required for it to work with KRO.
40+
* We need the ability to do partial applies of an applyset (without pruning) that is not supported by KDP library.
41+
* Also we need the ability to read-in the result of an apply for use by KRO engine. This requires some callback support.
42+
* We ended up adding call-backs (to populate KRO engine)
43+
* Also we enhanced applylib to support ExternalRef (read from cluster, no changes) - KRO specific for now.
44+
* Planning to add LifeCycle modifiers to support additional behaviours like abandon on delete, no update to a resource after creation etc.
45+
46+
Upstreaming:
47+
The goal is to upstream the changes back to either kubernetes-declarative-pattern or controller-runtime.
48+
We may need to do the enhancements and decide if all of it or some of it can be upstreamed.
49+
We will need to do a few iterations in the KRO repo first before we get an idea of where and how to upstream.
50+
*/
51+
package applyset

pkg/applyset/prune.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Copyright 2023 The Kubernetes Authors.
12
// Copyright 2025 The Kube Resource Orchestrator Authors
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,6 +35,14 @@ import (
3435
"k8s.io/client-go/dynamic"
3536
)
3637

38+
const (
39+
// This is set to an arbitrary number here for now.
40+
// This ensures we are no unbounded when pruning many GVKs.
41+
// Could be parameterized later on
42+
// TODO (barney-s): Possible parameterization target
43+
PruneGVKParallelizationLimit = 5
44+
)
45+
3746
// PruneObject is an apiserver object that should be deleted as part of prune.
3847
type PruneObject struct {
3948
*unstructured.Unstructured
@@ -64,7 +73,8 @@ func (a *applySet) FindAllObjectsToPrune(
6473
restMapping *meta.RESTMapping
6574
results []PruneObject
6675
}
67-
var tasks []*task
76+
77+
tasks := make([]*task, 0, len(a.desiredRESTMappings))
6878

6979
// We run discovery in parallel, in as many goroutines as priority and fairness will allow
7080
// (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds)
@@ -95,15 +105,15 @@ func (a *applySet) FindAllObjectsToPrune(
95105
}
96106

97107
group, ctx := errgroup.WithContext(ctx)
108+
group.SetLimit(PruneGVKParallelizationLimit)
98109
for i := range tasks {
99110
task := tasks[i]
100111
group.Go(func() error {
101112
results, err := a.findObjectsToPrune(ctx, dynamicClient, visitedUIDs, task.namespace, task.restMapping)
102113
if err != nil {
103114
return fmt.Errorf("listing %v objects for pruning: %w", task.restMapping.GroupVersionKind.String(), err)
104-
} else {
105-
task.results = results
106115
}
116+
task.results = results
107117
return nil
108118
})
109119
}

pkg/applyset/result.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright 2025 The Kube Resource Orchestrator Authors
2+
// Copyright 2022 The Kubernetes Authors.
23
//
34
// Licensed under the Apache License, Version 2.0 (the "License");
45
// you may not use this file except in compliance with the License.
@@ -12,6 +13,8 @@
1213
// See the License for the specific language governing permissions and
1314
// limitations under the License.
1415

16+
// Derived from: https://github.com/kubernetes-sigs/kubebuilder-declarative-pattern/blob/master/applylib/applyset/results.go
17+
1518
package applyset
1619

1720
import (
@@ -52,6 +55,11 @@ type PrunedObject struct {
5255
Error error
5356
}
5457

58+
// ApplyResult summarizes the results of an apply operation
59+
// It returns a list of applied and pruned objects
60+
// AppliedObject has both the input object and the result of the apply operation
61+
// Any errors returned by the apply call is also recorded in it.
62+
// PrunedObject records any error returned by the delete call.
5563
type ApplyResult struct {
5664
DesiredCount int
5765
AppliedObjects []AppliedObject

pkg/applyset/tracker.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ type Applyable interface {
4444
type ApplyableObject struct {
4545
*unstructured.Unstructured
4646

47+
// Optional
4748
// User provided unique identifier for the object.
49+
// If present a uniqeness check is done when adding
50+
// It is opaque and is passed in the callbacks as is
4851
ID string
4952

5053
// Lifecycle hints
@@ -104,12 +107,12 @@ func (t *tracker) Add(obj ApplyableObject) error {
104107
// TODO(barney-s): Do we need to care about client side uniqueness?
105108
// We could just not take the ID (opaque string) and let user deal with mapping
106109
// GVKNN to their ID. Adding a todo here to revisit this.
107-
108-
// client side uniqueness check
109-
if _, found := t.clientIDs[obj.ID]; found {
110-
return fmt.Errorf("duplicate object ID %v", obj.ID)
110+
if obj.ID != "" {
111+
if _, found := t.clientIDs[obj.ID]; found {
112+
return fmt.Errorf("duplicate object ID %v", obj.ID)
113+
}
114+
t.clientIDs[obj.ID] = true
111115
}
112-
t.clientIDs[obj.ID] = true
113116

114117
// Ensure the object is marshallable
115118
if _, err := json.Marshal(obj); err != nil {

pkg/client/fake/fake.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/kro-run/kro/pkg/client"
2121
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2222
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
23+
"k8s.io/apimachinery/pkg/api/meta"
2324
"k8s.io/client-go/dynamic"
2425
"k8s.io/client-go/kubernetes"
2526
"k8s.io/client-go/rest"
@@ -31,6 +32,7 @@ type FakeSet struct {
3132
KubernetesClient kubernetes.Interface
3233
ApiExtensionsClient apiextensionsv1.ApiextensionsV1Interface
3334
Config *rest.Config
35+
restMapper meta.RESTMapper
3436
}
3537

3638
var _ client.SetInterface = (*FakeSet)(nil)
@@ -75,6 +77,14 @@ func (f *FakeSet) WithImpersonation(user string) (client.SetInterface, error) {
7577
return f, nil
7678
}
7779

80+
func (f *FakeSet) RESTMapper() meta.RESTMapper {
81+
return f.restMapper
82+
}
83+
84+
func (f *FakeSet) SetRESTMapper(restMapper meta.RESTMapper) {
85+
f.restMapper = restMapper
86+
}
87+
7888
// FakeCRD is a fake implementation of CRDInterface for testing
7989
type FakeCRD struct{}
8090

pkg/client/set.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type SetInterface interface {
4646

4747
// WithImpersonation returns a new client that impersonates the given user
4848
WithImpersonation(user string) (SetInterface, error)
49+
50+
RESTMapper() meta.RESTMapper
51+
SetRESTMapper(restMapper meta.RESTMapper)
4952
}
5053

5154
// Set provides a unified interface for different Kubernetes clients
@@ -156,11 +159,6 @@ func (c *Set) RESTMapper() meta.RESTMapper {
156159
return c.restMapper
157160
}
158161

159-
// RESTMapper returns the REST mapper
160-
func (c *Set) RESTMapper() meta.RESTMapper {
161-
return c.restMapper
162-
}
163-
164162
// CRD returns a new CRDInterface instance
165163
func (c *Set) CRD(cfg CRDWrapperConfig) CRDInterface {
166164
if cfg.Client == nil {

0 commit comments

Comments
 (0)