Skip to content

Commit 3d88cf5

Browse files
committed
Update sync package to support creating objects it doesn't recognize
Update the sync package to support it attempting to create potentially arbitrary Kubernetes objects, as there are no restrictions (outside of RBAC) on what can be included in Kubernetes/OpenShift components. Signed-off-by: Angel Misevski <[email protected]>
1 parent c8a2154 commit 3d88cf5

File tree

4 files changed

+131
-17
lines changed

4 files changed

+131
-17
lines changed

pkg/provision/sync/cluster_api.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,17 @@ type UnrecoverableSyncError struct {
6767
func (e *UnrecoverableSyncError) Error() string {
6868
return e.Cause.Error()
6969
}
70+
71+
// WarningError is returned when syncing is successful and can continue but there is a warning
72+
// regarding the objects synced to the cluster (e.g. they will not be updated)
73+
type WarningError struct {
74+
Message string
75+
Err error
76+
}
77+
78+
func (e *WarningError) Error() string {
79+
if e.Err != nil {
80+
return fmt.Sprintf("%s: %s", e.Message, e.Err)
81+
}
82+
return e.Message
83+
}

pkg/provision/sync/diff.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ import (
3636
type diffFunc func(spec crclient.Object, cluster crclient.Object) (delete, update bool)
3737

3838
var diffFuncs = map[reflect.Type]diffFunc{
39-
reflect.TypeOf(rbacv1.Role{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(roleDiffOpts)),
40-
reflect.TypeOf(rbacv1.RoleBinding{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(rolebindingDiffOpts)),
41-
reflect.TypeOf(corev1.ServiceAccount{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, ownerrefsDiffFunc),
42-
reflect.TypeOf(appsv1.Deployment{}): allDiffFuncs(deploymentDiffFunc, labelsAndAnnotationsDiffFunc, basicDiffFunc(deploymentDiffOpts)),
43-
reflect.TypeOf(corev1.ConfigMap{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(configmapDiffOpts)),
44-
reflect.TypeOf(corev1.Secret{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(secretDiffOpts)),
45-
reflect.TypeOf(v1alpha1.DevWorkspaceRouting{}): allDiffFuncs(routingDiffFunc, labelsAndAnnotationsDiffFunc, basicDiffFunc(routingDiffOpts)),
46-
reflect.TypeOf(batchv1.Job{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, jobDiffFunc),
47-
reflect.TypeOf(corev1.Service{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, serviceDiffFunc),
48-
reflect.TypeOf(networkingv1.Ingress{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(ingressDiffOpts)),
49-
reflect.TypeOf(routev1.Route{}): allDiffFuncs(labelsAndAnnotationsDiffFunc, basicDiffFunc(routeDiffOpts)),
39+
reflect.TypeOf(rbacv1.Role{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(roleDiffOpts)),
40+
reflect.TypeOf(rbacv1.RoleBinding{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(rolebindingDiffOpts)),
41+
reflect.TypeOf(corev1.ServiceAccount{}): metadataDiffFunc,
42+
reflect.TypeOf(appsv1.Deployment{}): allDiffFuncs(deploymentDiffFunc, metadataDiffFunc, basicDiffFunc(deploymentDiffOpts)),
43+
reflect.TypeOf(corev1.ConfigMap{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(configmapDiffOpts)),
44+
reflect.TypeOf(corev1.Secret{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(secretDiffOpts)),
45+
reflect.TypeOf(v1alpha1.DevWorkspaceRouting{}): allDiffFuncs(routingDiffFunc, metadataDiffFunc, basicDiffFunc(routingDiffOpts)),
46+
reflect.TypeOf(batchv1.Job{}): allDiffFuncs(metadataDiffFunc, jobDiffFunc),
47+
reflect.TypeOf(corev1.Service{}): allDiffFuncs(metadataDiffFunc, serviceDiffFunc),
48+
reflect.TypeOf(networkingv1.Ingress{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(ingressDiffOpts)),
49+
reflect.TypeOf(routev1.Route{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(routeDiffOpts)),
5050
}
5151

5252
// basicDiffFunc returns a diffFunc that specifies an object needs an update if cmp.Equal fails
@@ -56,9 +56,9 @@ func basicDiffFunc(diffOpt cmp.Options) diffFunc {
5656
}
5757
}
5858

59-
// labelsAndAnnotationsDiffFunc requires an object to be updated if any label or annotation present in the spec
59+
// metadataDiffFunc requires an object to be updated if any label or annotation present in the spec
6060
// object is not present in the cluster object.
61-
func labelsAndAnnotationsDiffFunc(spec, cluster crclient.Object) (delete, update bool) {
61+
func metadataDiffFunc(spec, cluster crclient.Object) (delete, update bool) {
6262
clusterAnnotations := cluster.GetAnnotations()
6363
for k, v := range spec.GetAnnotations() {
6464
if clusterAnnotations[k] != v {
@@ -71,10 +71,6 @@ func labelsAndAnnotationsDiffFunc(spec, cluster crclient.Object) (delete, update
7171
return false, true
7272
}
7373
}
74-
return false, false
75-
}
76-
77-
func ownerrefsDiffFunc(spec, cluster crclient.Object) (delete, update bool) {
7874
clusterRefs := cluster.GetOwnerReferences()
7975
for _, ownerref := range spec.GetOwnerReferences() {
8076
if !containsOwnerRef(ownerref, clusterRefs) {
@@ -147,6 +143,10 @@ func serviceDiffFunc(spec, cluster crclient.Object) (delete, update bool) {
147143
return false, specCopy.Spec.Type != clusterCopy.Spec.Type
148144
}
149145

146+
func unrecognizedObjectDiffFunc(spec, cluster crclient.Object) (delete, update bool) {
147+
return metadataDiffFunc(spec, cluster)
148+
}
149+
150150
func containsOwnerRef(toCheck metav1.OwnerReference, listRefs []metav1.OwnerReference) bool {
151151
boolPtrsEqual := func(a, b *bool) bool {
152152
// If either is nil, assume check other is nil or false; otherwise, compare actual values

pkg/provision/sync/sync.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ import (
3131
crclient "sigs.k8s.io/controller-runtime/pkg/client"
3232
)
3333

34+
// IsRecognizedObject returns whether the provided object kind is recognized by the sync package to support updating
35+
// existing objects on the cluster. If the object is not recognized, passing it to SyncObjectWithCluster will fail.
36+
// Instead, SyncUnrecognizedObjectWithCluster should be used.
37+
func IsRecognizedObject(specObj crclient.Object) bool {
38+
objType := reflect.TypeOf(specObj).Elem()
39+
_, ok := diffFuncs[objType]
40+
return ok
41+
}
42+
3443
// SyncObjectWithCluster synchronises the state of specObj to the cluster, creating or updating the cluster object
3544
// as required. If specObj is in sync with the cluster, returns the object as it exists on the cluster. Returns a
3645
// NotInSyncError if an update is required, UnrecoverableSyncError if object provided is invalid, or generic error
@@ -72,6 +81,59 @@ func SyncObjectWithCluster(specObj crclient.Object, api ClusterAPI) (crclient.Ob
7281
return clusterObj, nil
7382
}
7483

84+
// SyncUnrecognizedObjectWithCluster allows syncing objects not supported by SyncObjectWithCluster. As there is
85+
// no generic way of deciding if an object needs to be updated, a WarningError is returned if the object exists
86+
// on the cluster. The only object updating performed by this function is to ensure labels/annotations and
87+
// ownerReferences in specObj are synced to the cluster.
88+
// The reason arbitrary updates are not supported is 1) certain objects have defaulted fields that can always
89+
// trigger naive diff checks (causing reconciles to get stuck), and 2) it's unknown which fields are unmodifiable,
90+
// (e.g. services must keep ClusterIP once set; pod fields cannot be changed after creation)
91+
func SyncUnrecognizedObjectWithCluster(specObj crclient.Object, api ClusterAPI) (crclient.Object, error) {
92+
objType := reflect.TypeOf(specObj).Elem()
93+
clusterObj := reflect.New(objType).Interface().(crclient.Object)
94+
95+
err := api.Client.Get(api.Ctx, types.NamespacedName{Name: specObj.GetName(), Namespace: specObj.GetNamespace()}, clusterObj)
96+
if err != nil {
97+
if k8sErrors.IsNotFound(err) {
98+
return nil, createObjectGeneric(specObj, api)
99+
}
100+
return nil, err
101+
}
102+
update, delete := unrecognizedObjectDiffFunc(specObj, clusterObj)
103+
if update {
104+
toUpdate, err := unrecognizedObjectUpdateFunc(specObj, clusterObj)
105+
if err != nil {
106+
return nil, &UnrecoverableSyncError{err}
107+
}
108+
err = api.Client.Update(api.Ctx, toUpdate)
109+
switch {
110+
case err == nil:
111+
api.Logger.Info("Updated object", "kind", reflect.TypeOf(specObj).Elem().String(), "name", specObj.GetName())
112+
return nil, NewNotInSync(specObj, UpdatedObjectReason)
113+
case k8sErrors.IsConflict(err):
114+
return nil, NewNotInSync(specObj, NeedRetryReason)
115+
case k8sErrors.IsInvalid(err), k8sErrors.IsForbidden(err):
116+
return nil, &UnrecoverableSyncError{err}
117+
default:
118+
return nil, err
119+
}
120+
}
121+
if delete {
122+
if err := api.Client.Delete(api.Ctx, clusterObj); err != nil {
123+
if k8sErrors.IsForbidden(err) {
124+
return nil, &UnrecoverableSyncError{fmt.Errorf("failed to delete object %s: %w", clusterObj.GetName(), err)}
125+
}
126+
return nil, err
127+
}
128+
api.Logger.Info("Deleted object", "kind", reflect.TypeOf(specObj).Elem().String(), "name", specObj.GetName())
129+
return nil, NewNotInSync(specObj, DeletedObjectReason)
130+
}
131+
kind := specObj.GetObjectKind().GroupVersionKind().GroupKind().String()
132+
return clusterObj, &WarningError{
133+
Message: fmt.Sprintf("Unrecognized object kind %s. Object will not be updated on cluster", kind),
134+
}
135+
}
136+
75137
func createObjectGeneric(specObj crclient.Object, api ClusterAPI) error {
76138
err := api.Client.Create(api.Ctx, specObj)
77139
switch {

pkg/provision/sync/update.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"reflect"
1919

2020
corev1 "k8s.io/api/core/v1"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
2122
crclient "sigs.k8s.io/controller-runtime/pkg/client"
2223
)
2324

@@ -77,3 +78,40 @@ func getUpdateFunc(obj crclient.Object) updateFunc {
7778
return defaultUpdateFunc
7879
}
7980
}
81+
82+
func unrecognizedObjectUpdateFunc(spec, cluster crclient.Object) (crclient.Object, error) {
83+
if cluster == nil {
84+
return nil, errors.New("updating unrecognized object requires the cluster instance")
85+
}
86+
clusterCopy := cluster.DeepCopyObject().(client.Object)
87+
newLabels := clusterCopy.GetLabels()
88+
for k, v := range spec.GetLabels() {
89+
newLabels[k] = v
90+
}
91+
clusterCopy.SetLabels(newLabels)
92+
93+
newAnnotations := clusterCopy.GetAnnotations()
94+
for k, v := range spec.GetAnnotations() {
95+
newAnnotations[k] = v
96+
}
97+
clusterCopy.SetAnnotations(newAnnotations)
98+
99+
newOwnerrefs := clusterCopy.GetOwnerReferences()
100+
for _, specOwnerref := range spec.GetOwnerReferences() {
101+
found := false
102+
for _, ownerref := range cluster.GetOwnerReferences() {
103+
if specOwnerref.Kind == ownerref.Kind &&
104+
specOwnerref.Name == ownerref.Name &&
105+
specOwnerref.UID == ownerref.UID {
106+
found = true
107+
break
108+
}
109+
}
110+
if !found {
111+
newOwnerrefs = append(newOwnerrefs, specOwnerref)
112+
}
113+
}
114+
clusterCopy.SetOwnerReferences(newOwnerrefs)
115+
116+
return clusterCopy, nil
117+
}

0 commit comments

Comments
 (0)