Skip to content

Commit 5e96566

Browse files
authored
Merge pull request #7497 from g-gaston/crs-reconcile
✨ Implement Reconcile mode for ClusterResourceSet
2 parents 9e00c95 + c32cf26 commit 5e96566

11 files changed

+1076
-155
lines changed

config/crd/bases/addons.cluster.x-k8s.io_clusterresourcesets.yaml

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

docs/book/src/tasks/experimental-features/cluster-resource-set.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ The `ClusterResourceSet` feature is introduced to provide a way to automatically
88

99
More details on `ClusterResourceSet` and an example to test it can be found at:
1010
[ClusterResourceSet CAEP](https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20200220-cluster-resource-set.md)
11+
12+
## Update from `ApplyOnce` to `Reconcile`
13+
14+
The `strategy` field is immutable so existing CRS can't be updated directly. However, CAPI won't delete the managed resources in the target cluster when the CRS is deleted.
15+
So if you want to start using the `Reconcile` strategy, delete your existing CRS and create it again with the updated `strategy`.

exp/addons/api/v1beta1/clusterresourceset_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type ClusterResourceSetSpec struct {
4646
Resources []ResourceRef `json:"resources,omitempty"`
4747

4848
// Strategy is the strategy to be used during applying resources. Defaults to ApplyOnce. This field is immutable.
49-
// +kubebuilder:validation:Enum=ApplyOnce
49+
// +kubebuilder:validation:Enum=ApplyOnce;Reconcile
5050
// +optional
5151
Strategy string `json:"strategy,omitempty"`
5252
}
@@ -80,6 +80,9 @@ const (
8080
// ClusterResourceSetStrategyApplyOnce is the default strategy a ClusterResourceSet strategy is assigned by
8181
// ClusterResourceSet controller after being created if not specified by user.
8282
ClusterResourceSetStrategyApplyOnce ClusterResourceSetStrategy = "ApplyOnce"
83+
// ClusterResourceSetStrategyReconcile reapplies the resources managed by a ClusterResourceSet
84+
// if their normalized hash changes.
85+
ClusterResourceSetStrategyReconcile ClusterResourceSetStrategy = "Reconcile"
8386
)
8487

8588
// SetTypedStrategy sets the Strategy field to the string representation of ClusterResourceSetStrategy.

exp/addons/api/v1beta1/clusterresourcesetbinding_types.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,18 @@ type ResourceSetBinding struct {
5858

5959
// IsApplied returns true if the resource is applied to the cluster by checking the cluster's binding.
6060
func (r *ResourceSetBinding) IsApplied(resourceRef ResourceRef) bool {
61+
resourceBinding := r.GetResource(resourceRef)
62+
return resourceBinding != nil && resourceBinding.Applied
63+
}
64+
65+
// GetResource returns a ResourceBinding for a resource ref if present.
66+
func (r *ResourceSetBinding) GetResource(resourceRef ResourceRef) *ResourceBinding {
6167
for _, resource := range r.Resources {
6268
if reflect.DeepEqual(resource.ResourceRef, resourceRef) {
63-
if resource.Applied {
64-
return true
65-
}
69+
return &resource
6670
}
6771
}
68-
return false
72+
return nil
6973
}
7074

7175
// SetBinding sets resourceBinding for a resource in resourceSetbinding either by updating the existing one or

exp/addons/api/v1beta1/clusterresourcesetbinding_types_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,66 @@ func TestIsResourceApplied(t *testing.T) {
9090
}
9191
}
9292

93+
func TestResourceSetBindingGetResourceBinding(t *testing.T) {
94+
resourceRefApplyFailed := ResourceRef{
95+
Name: "applyFailed",
96+
Kind: "Secret",
97+
}
98+
resourceRefApplySucceeded := ResourceRef{
99+
Name: "ApplySucceeded",
100+
Kind: "Secret",
101+
}
102+
resourceRefNotExist := ResourceRef{
103+
Name: "notExist",
104+
Kind: "Secret",
105+
}
106+
107+
resourceRefApplyFailedBinding := ResourceBinding{
108+
ResourceRef: resourceRefApplyFailed,
109+
Applied: false,
110+
Hash: "",
111+
LastAppliedTime: &metav1.Time{Time: time.Now().UTC()},
112+
}
113+
crsBinding := &ResourceSetBinding{
114+
ClusterResourceSetName: "test-clusterResourceSet",
115+
Resources: []ResourceBinding{
116+
{
117+
ResourceRef: resourceRefApplySucceeded,
118+
Applied: true,
119+
Hash: "xyz",
120+
LastAppliedTime: &metav1.Time{Time: time.Now().UTC()},
121+
},
122+
resourceRefApplyFailedBinding,
123+
},
124+
}
125+
126+
tests := []struct {
127+
name string
128+
resourceSetBinding *ResourceSetBinding
129+
resourceRef ResourceRef
130+
want *ResourceBinding
131+
}{
132+
{
133+
name: "ResourceRef doesn't exist",
134+
resourceSetBinding: crsBinding,
135+
resourceRef: resourceRefNotExist,
136+
want: nil,
137+
},
138+
{
139+
name: "ResourceRef exists",
140+
resourceSetBinding: crsBinding,
141+
resourceRef: resourceRefApplyFailed,
142+
want: &resourceRefApplyFailedBinding,
143+
},
144+
}
145+
for _, tt := range tests {
146+
t.Run(tt.name, func(t *testing.T) {
147+
gs := NewWithT(t)
148+
gs.Expect(tt.resourceSetBinding.GetResource(tt.resourceRef)).To(Equal(tt.want))
149+
})
150+
}
151+
}
152+
93153
func TestSetResourceBinding(t *testing.T) {
94154
resourceRefApplyFailed := ResourceRef{
95155
Name: "applyFailed",

exp/addons/internal/controllers/clusterresourceset_controller.go

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21-
"encoding/base64"
2221
"fmt"
23-
"sort"
2422
"time"
2523

2624
"github.com/pkg/errors"
@@ -51,10 +49,8 @@ import (
5149
"sigs.k8s.io/cluster-api/util/predicates"
5250
)
5351

54-
var (
55-
// ErrSecretTypeNotSupported signals that a Secret is not supported.
56-
ErrSecretTypeNotSupported = errors.New("unsupported secret type")
57-
)
52+
// ErrSecretTypeNotSupported signals that a Secret is not supported.
53+
var ErrSecretTypeNotSupported = errors.New("unsupported secret type")
5854

5955
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;patch
6056
// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;patch
@@ -82,21 +78,20 @@ func (r *ClusterResourceSetReconciler) SetupWithManager(ctx context.Context, mgr
8278
handler.EnqueueRequestsFromMapFunc(r.resourceToClusterResourceSet),
8379
builder.OnlyMetadata,
8480
builder.WithPredicates(
85-
resourcepredicates.ResourceCreate(ctrl.LoggerFrom(ctx)),
81+
resourcepredicates.ResourceCreateOrUpdate(ctrl.LoggerFrom(ctx)),
8682
),
8783
).
8884
Watches(
8985
&source.Kind{Type: &corev1.Secret{}},
9086
handler.EnqueueRequestsFromMapFunc(r.resourceToClusterResourceSet),
9187
builder.OnlyMetadata,
9288
builder.WithPredicates(
93-
resourcepredicates.ResourceCreate(ctrl.LoggerFrom(ctx)),
89+
resourcepredicates.ResourceCreateOrUpdate(ctrl.LoggerFrom(ctx)),
9490
),
9591
).
9692
WithOptions(options).
9793
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
9894
Complete(r)
99-
10095
if err != nil {
10196
return errors.Wrap(err, "failed setting up with a controller manager")
10297
}
@@ -241,6 +236,8 @@ func (r *ClusterResourceSetReconciler) getClustersByClusterResourceSetSelector(c
241236
// cluster's ClusterResourceSetBinding.
242237
// In ApplyOnce strategy, resources are applied only once to a particular cluster. ClusterResourceSetBinding is used to check if a resource is applied before.
243238
// It applies resources best effort and continue on scenarios like: unsupported resource types, failure during creation, missing resources.
239+
// In Reconcile strategy, resources are re-applied to a particular cluster when their definition changes. The hash in ClusterResourceSetBinding is used to check
240+
// if a resource has changed or not.
244241
// TODO: If a resource already exists in the cluster but not applied by ClusterResourceSet, the resource will be updated ?
245242
func (r *ClusterResourceSetReconciler) ApplyClusterResourceSet(ctx context.Context, cluster *clusterv1.Cluster, clusterResourceSet *addonsv1.ClusterResourceSet) error {
246243
log := ctrl.LoggerFrom(ctx, "Cluster", klog.KObj(cluster))
@@ -301,8 +298,20 @@ func (r *ClusterResourceSetReconciler) ApplyClusterResourceSet(ctx context.Conte
301298
errList = append(errList, err)
302299
}
303300

304-
// If resource is already applied successfully and clusterResourceSet mode is "ApplyOnce", continue. (No need to check hash changes here)
305-
if resourceSetBinding.IsApplied(resource) {
301+
resourceScope, err := reconcileScopeForResource(clusterResourceSet, resource, resourceSetBinding, unstructuredObj)
302+
if err != nil {
303+
resourceSetBinding.SetBinding(addonsv1.ResourceBinding{
304+
ResourceRef: resource,
305+
Hash: "",
306+
Applied: false,
307+
LastAppliedTime: &metav1.Time{Time: time.Now().UTC()},
308+
})
309+
310+
errList = append(errList, err)
311+
continue
312+
}
313+
314+
if !resourceScope.needsApply() {
306315
continue
307316
}
308317

@@ -314,54 +323,20 @@ func (r *ClusterResourceSetReconciler) ApplyClusterResourceSet(ctx context.Conte
314323
Applied: false,
315324
LastAppliedTime: &metav1.Time{Time: time.Now().UTC()},
316325
})
317-
// Since maps are not ordered, we need to order them to get the same hash at each reconcile.
318-
keys := make([]string, 0)
319-
data, ok := unstructuredObj.UnstructuredContent()["data"]
320-
if !ok {
321-
errList = append(errList, errors.New("failed to get data field from the resource"))
322-
continue
323-
}
324-
325-
unstructuredData := data.(map[string]interface{})
326-
for key := range unstructuredData {
327-
keys = append(keys, key)
328-
}
329-
sort.Strings(keys)
330-
331-
dataList := make([][]byte, 0)
332-
for _, key := range keys {
333-
val, ok, err := unstructured.NestedString(unstructuredData, key)
334-
if !ok || err != nil {
335-
errList = append(errList, errors.New("failed to get value field from the resource"))
336-
continue
337-
}
338-
339-
byteArr := []byte(val)
340-
// If the resource is a Secret, data needs to be decoded.
341-
if unstructuredObj.GetKind() == string(addonsv1.SecretClusterResourceSetResourceKind) {
342-
byteArr, _ = base64.StdEncoding.DecodeString(val)
343-
}
344-
345-
dataList = append(dataList, byteArr)
346-
}
347326

348327
// Apply all values in the key-value pair of the resource to the cluster.
349328
// As there can be multiple key-value pairs in a resource, each value may have multiple objects in it.
350329
isSuccessful := true
351-
for i := range dataList {
352-
data := dataList[i]
353-
354-
if err := apply(ctx, remoteClient, data); err != nil {
355-
isSuccessful = false
356-
log.Error(err, "failed to apply ClusterResourceSet resource", "Resource kind", resource.Kind, "Resource name", resource.Name)
357-
conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.ApplyFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
358-
errList = append(errList, err)
359-
}
330+
if err := resourceScope.apply(ctx, remoteClient); err != nil {
331+
isSuccessful = false
332+
log.Error(err, "failed to apply ClusterResourceSet resource", "Resource kind", resource.Kind, "Resource name", resource.Name)
333+
conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.ApplyFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
334+
errList = append(errList, err)
360335
}
361336

362337
resourceSetBinding.SetBinding(addonsv1.ResourceBinding{
363338
ResourceRef: resource,
364-
Hash: computeHash(dataList),
339+
Hash: resourceScope.hash(),
365340
Applied: isSuccessful,
366341
LastAppliedTime: &metav1.Time{Time: time.Now().UTC()},
367342
})

0 commit comments

Comments
 (0)