Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Currently the operator uses Server-Side Apply (SSA)[1] to manage its resources. Unfortunately, this approach has some drawbacks as it allows users to modify resources independently from the operator. To prevent this behavior, this commit adds custom apply functions inspired by ones from resourseapply module in library-go[2]. [1] https://kubernetes.io/docs/reference/using-api/server-side-apply/ [2] https://github.com/openshift/library-go/tree/master/pkg/operator/resource/resourceapply
- Loading branch information
Showing
2 changed files
with
239 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
package controllers | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/sha256" | ||
"encoding/json" | ||
"fmt" | ||
|
||
appsv1 "k8s.io/api/apps/v1" | ||
corev1 "k8s.io/api/core/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/tools/record" | ||
|
||
coreclientv1 "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge" | ||
) | ||
|
||
const specHashAnnotation = "operator.openshift.io/spec-hash" | ||
|
||
// SetSpecHashAnnotation computes the hash of the provided spec and sets an annotation of the | ||
// hash on the provided ObjectMeta. This method is used internally by Apply<type> methods, and | ||
// is exposed to support testing with fake clients that need to know the mutated form of the | ||
// resource resulting from an Apply<type> call. | ||
func SetSpecHashAnnotation(objMeta *metav1.ObjectMeta, spec interface{}) error { | ||
jsonBytes, err := json.Marshal(spec) | ||
if err != nil { | ||
return err | ||
} | ||
specHash := fmt.Sprintf("%x", sha256.Sum256(jsonBytes)) | ||
if objMeta.Annotations == nil { | ||
objMeta.Annotations = map[string]string{} | ||
} | ||
objMeta.Annotations[specHashAnnotation] = specHash | ||
return nil | ||
} | ||
|
||
// ApplyConfigMap merges objectmeta, requires data | ||
func ApplyConfigMap(ctx context.Context, client coreclientv1.Client, recorder record.EventRecorder, required *corev1.ConfigMap) (bool, error) { | ||
existing := &corev1.ConfigMap{} | ||
err := client.Get(ctx, coreclientv1.ObjectKeyFromObject(required), existing) | ||
if apierrors.IsNotFound(err) { | ||
requiredCopy := required.DeepCopy() | ||
err := client.Create(ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*corev1.ConfigMap)) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(requiredCopy, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, nil | ||
} | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
|
||
modified := resourcemerge.BoolPtr(false) | ||
existingCopy := existing.DeepCopy() | ||
|
||
resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) | ||
|
||
caBundleInjected := required.Labels["config.openshift.io/inject-trusted-cabundle"] == "true" | ||
_, newCABundleRequired := required.Data["ca-bundle.crt"] | ||
|
||
var modifiedKeys []string | ||
for existingCopyKey, existingCopyValue := range existingCopy.Data { | ||
// if we're injecting a ca-bundle and the required isn't forcing the value, then don't use the value of existing | ||
// to drive a diff detection. If required has set the value then we need to force the value in order to have apply | ||
// behave predictably. | ||
if caBundleInjected && !newCABundleRequired && existingCopyKey == "ca-bundle.crt" { | ||
continue | ||
} | ||
if requiredValue, ok := required.Data[existingCopyKey]; !ok || (existingCopyValue != requiredValue) { | ||
modifiedKeys = append(modifiedKeys, "data."+existingCopyKey) | ||
} | ||
} | ||
for existingCopyKey, existingCopyBinValue := range existingCopy.BinaryData { | ||
if requiredBinValue, ok := required.BinaryData[existingCopyKey]; !ok || !bytes.Equal(existingCopyBinValue, requiredBinValue) { | ||
modifiedKeys = append(modifiedKeys, "binaryData."+existingCopyKey) | ||
} | ||
} | ||
for requiredKey := range required.Data { | ||
if _, ok := existingCopy.Data[requiredKey]; !ok { | ||
modifiedKeys = append(modifiedKeys, "data."+requiredKey) | ||
} | ||
} | ||
for requiredBinKey := range required.BinaryData { | ||
if _, ok := existingCopy.BinaryData[requiredBinKey]; !ok { | ||
modifiedKeys = append(modifiedKeys, "binaryData."+requiredBinKey) | ||
} | ||
} | ||
|
||
dataSame := len(modifiedKeys) == 0 | ||
if dataSame && !*modified { | ||
return false, nil | ||
} | ||
existingCopy.Data = required.Data | ||
existingCopy.BinaryData = required.BinaryData | ||
// if we're injecting a cabundle, and we had a previous value, and the required object isn't setting the value, then set back to the previous | ||
if existingCABundle, existedBefore := existing.Data["ca-bundle.crt"]; caBundleInjected && existedBefore && !newCABundleRequired { | ||
if existingCopy.Data == nil { | ||
existingCopy.Data = map[string]string{} | ||
} | ||
existingCopy.Data["ca-bundle.crt"] = existingCABundle | ||
} | ||
|
||
// at this point we know that we're going to perform a write. We're just trying to get the object correct | ||
toWrite := existingCopy // shallow copy so the code reads easier | ||
|
||
err = client.Update(ctx, toWrite) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(toWrite, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, err | ||
} | ||
|
||
func ApplyDeployment(ctx context.Context, client coreclientv1.Client, recorder record.EventRecorder, requiredOriginal *appsv1.Deployment) (bool, error) { | ||
required := requiredOriginal.DeepCopy() | ||
err := SetSpecHashAnnotation(&required.ObjectMeta, required.Spec) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
if required.Annotations == nil { | ||
required.Annotations = map[string]string{} | ||
} | ||
if _, ok := required.Annotations[specHashAnnotation]; !ok { | ||
// If the spec hash annotation is not present, the caller expects the | ||
// pull-spec annotation to be applied. | ||
required.Annotations["operator.openshift.io/pull-spec"] = required.Spec.Template.Spec.Containers[0].Image | ||
} | ||
|
||
existing := &appsv1.Deployment{} | ||
err = client.Get(ctx, coreclientv1.ObjectKeyFromObject(required), existing) | ||
if apierrors.IsNotFound(err) { | ||
err := client.Create(ctx, required) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(required, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, nil | ||
} | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
|
||
modified := resourcemerge.BoolPtr(false) | ||
existingCopy := existing.DeepCopy() | ||
|
||
resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) | ||
if !*modified { | ||
return false, nil | ||
} | ||
|
||
// at this point we know that we're going to perform a write. We're just trying to get the object correct | ||
toWrite := existingCopy // shallow copy so the code reads easier | ||
toWrite.Spec = *required.Spec.DeepCopy() | ||
|
||
err = client.Update(ctx, toWrite) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(required, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, nil | ||
} | ||
|
||
func ApplyDaemonSet(ctx context.Context, client coreclientv1.Client, recorder record.EventRecorder, requiredOriginal *appsv1.DaemonSet) (bool, error) { | ||
|
||
required := requiredOriginal.DeepCopy() | ||
err := SetSpecHashAnnotation(&required.ObjectMeta, required.Spec) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
if required.Annotations == nil { | ||
required.Annotations = map[string]string{} | ||
} | ||
if _, ok := required.Annotations[specHashAnnotation]; !ok { | ||
// If the spec hash annotation is not present, the caller expects the | ||
// pull-spec annotation to be applied. | ||
required.Annotations["operator.openshift.io/pull-spec"] = required.Spec.Template.Spec.Containers[0].Image | ||
} | ||
|
||
existing := &appsv1.DaemonSet{} | ||
err = client.Get(ctx, coreclientv1.ObjectKeyFromObject(required), existing) | ||
if apierrors.IsNotFound(err) { | ||
err = client.Create(ctx, required) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(required, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, nil | ||
} | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
|
||
modified := resourcemerge.BoolPtr(false) | ||
existingCopy := existing.DeepCopy() | ||
|
||
resourcemerge.EnsureObjectMeta(modified, &existingCopy.ObjectMeta, required.ObjectMeta) | ||
if !*modified { | ||
return false, nil | ||
} | ||
|
||
// at this point we know that we're going to perform a write. We're just trying to get the object correct | ||
toWrite := existingCopy // shallow copy so the code reads easier | ||
toWrite.Spec = *required.Spec.DeepCopy() | ||
|
||
err = client.Update(ctx, toWrite) | ||
if err != nil { | ||
recorder.Event(required, corev1.EventTypeWarning, "Update failed", err.Error()) | ||
return false, err | ||
} | ||
recorder.Event(required, corev1.EventTypeNormal, "Updated successfully", "Resource was successfully updated") | ||
return true, nil | ||
} |