internal/manifests/mutate.go (266 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package manifests
import (
"errors"
"fmt"
"reflect"
"dario.cat/mergo"
routev1 "github.com/openshift/api/route/v1"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
policyV1 "k8s.io/api/policy/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
var (
ImmutableChangeErr = errors.New("immutable field change attempted")
)
// MutateFuncFor returns a mutate function based on the
// existing resource's concrete type. It supports currently
// only the following types or else panics:
// - ConfigMap
// - Service
// - ServiceAccount
// - ClusterRole
// - ClusterRoleBinding
// - Role
// - RoleBinding
// - Deployment
// - DaemonSet
// - StatefulSet
// - ServiceMonitor
// - Ingress
// - HorizontalPodAutoscaler
// - Route
// - Secret
// In order for the operator to reconcile other types, they must be added here.
// The function returned takes no arguments but instead uses the existing and desired inputs here. Existing is expected
// to be set by the controller-runtime package through a client get call.
func MutateFuncFor(existing, desired client.Object) controllerutil.MutateFn {
return func() error {
// Get the existing annotations and override any conflicts with the desired annotations
// This will preserve any annotations on the existing set.
existingAnnotations := existing.GetAnnotations()
if err := mergeWithOverride(&existingAnnotations, desired.GetAnnotations()); err != nil {
return err
}
existing.SetAnnotations(existingAnnotations)
// Get the existing labels and override any conflicts with the desired labels
// This will preserve any labels on the existing set.
existingLabels := existing.GetLabels()
if err := mergeWithOverride(&existingLabels, desired.GetLabels()); err != nil {
return err
}
existing.SetLabels(existingLabels)
if ownerRefs := desired.GetOwnerReferences(); len(ownerRefs) > 0 {
existing.SetOwnerReferences(ownerRefs)
}
switch existing.(type) {
case *corev1.ConfigMap:
cm := existing.(*corev1.ConfigMap)
wantCm := desired.(*corev1.ConfigMap)
mutateConfigMap(cm, wantCm)
case *corev1.Service:
svc := existing.(*corev1.Service)
wantSvc := desired.(*corev1.Service)
return mutateService(svc, wantSvc)
case *corev1.ServiceAccount:
sa := existing.(*corev1.ServiceAccount)
wantSa := desired.(*corev1.ServiceAccount)
mutateServiceAccount(sa, wantSa)
case *rbacv1.ClusterRole:
cr := existing.(*rbacv1.ClusterRole)
wantCr := desired.(*rbacv1.ClusterRole)
mutateClusterRole(cr, wantCr)
case *rbacv1.ClusterRoleBinding:
crb := existing.(*rbacv1.ClusterRoleBinding)
wantCrb := desired.(*rbacv1.ClusterRoleBinding)
mutateClusterRoleBinding(crb, wantCrb)
case *rbacv1.Role:
r := existing.(*rbacv1.Role)
wantR := desired.(*rbacv1.Role)
mutateRole(r, wantR)
case *rbacv1.RoleBinding:
rb := existing.(*rbacv1.RoleBinding)
wantRb := desired.(*rbacv1.RoleBinding)
mutateRoleBinding(rb, wantRb)
case *appsv1.Deployment:
dpl := existing.(*appsv1.Deployment)
wantDpl := desired.(*appsv1.Deployment)
return mutateDeployment(dpl, wantDpl)
case *appsv1.DaemonSet:
dpl := existing.(*appsv1.DaemonSet)
wantDpl := desired.(*appsv1.DaemonSet)
return mutateDaemonset(dpl, wantDpl)
case *appsv1.StatefulSet:
sts := existing.(*appsv1.StatefulSet)
wantSts := desired.(*appsv1.StatefulSet)
return mutateStatefulSet(sts, wantSts)
case *monitoringv1.ServiceMonitor:
svcMonitor := existing.(*monitoringv1.ServiceMonitor)
wantSvcMonitor := desired.(*monitoringv1.ServiceMonitor)
mutateServiceMonitor(svcMonitor, wantSvcMonitor)
case *monitoringv1.PodMonitor:
podMonitor := existing.(*monitoringv1.PodMonitor)
wantPodMonitor := desired.(*monitoringv1.PodMonitor)
mutatePodMonitor(podMonitor, wantPodMonitor)
case *networkingv1.Ingress:
ing := existing.(*networkingv1.Ingress)
wantIng := desired.(*networkingv1.Ingress)
mutateIngress(ing, wantIng)
case *autoscalingv2.HorizontalPodAutoscaler:
existingHPA := existing.(*autoscalingv2.HorizontalPodAutoscaler)
desiredHPA := desired.(*autoscalingv2.HorizontalPodAutoscaler)
mutateAutoscalingHPA(existingHPA, desiredHPA)
case *policyV1.PodDisruptionBudget:
existingPDB := existing.(*policyV1.PodDisruptionBudget)
desiredPDB := desired.(*policyV1.PodDisruptionBudget)
mutatePolicyV1PDB(existingPDB, desiredPDB)
case *routev1.Route:
rt := existing.(*routev1.Route)
wantRt := desired.(*routev1.Route)
mutateRoute(rt, wantRt)
case *corev1.Secret:
pr := existing.(*corev1.Secret)
wantPr := desired.(*corev1.Secret)
mutateSecret(pr, wantPr)
default:
t := reflect.TypeOf(existing).String()
return fmt.Errorf("missing mutate implementation for resource type: %s", t)
}
return nil
}
}
func mergeWithOverride(dst, src interface{}) error {
return mergo.Merge(dst, src, mergo.WithOverride)
}
func mutateSecret(existing, desired *corev1.Secret) {
existing.Labels = desired.Labels
existing.Annotations = desired.Annotations
existing.Data = desired.Data
}
func mutateConfigMap(existing, desired *corev1.ConfigMap) {
existing.BinaryData = desired.BinaryData
existing.Data = desired.Data
}
func mutateServiceAccount(existing, desired *corev1.ServiceAccount) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
}
func mutateClusterRole(existing, desired *rbacv1.ClusterRole) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Rules = desired.Rules
}
func mutateClusterRoleBinding(existing, desired *rbacv1.ClusterRoleBinding) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Subjects = desired.Subjects
}
func mutateRole(existing, desired *rbacv1.Role) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Rules = desired.Rules
}
func mutateRoleBinding(existing, desired *rbacv1.RoleBinding) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Subjects = desired.Subjects
}
func mutateAutoscalingHPA(existing, desired *autoscalingv2.HorizontalPodAutoscaler) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Spec = desired.Spec
}
func mutatePolicyV1PDB(existing, desired *policyV1.PodDisruptionBudget) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Spec = desired.Spec
}
func mutateIngress(existing, desired *networkingv1.Ingress) {
existing.Labels = desired.Labels
existing.Annotations = desired.Annotations
existing.Spec.DefaultBackend = desired.Spec.DefaultBackend
existing.Spec.Rules = desired.Spec.Rules
existing.Spec.TLS = desired.Spec.TLS
}
func mutateRoute(existing, desired *routev1.Route) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Spec = desired.Spec
}
func mutateServiceMonitor(existing, desired *monitoringv1.ServiceMonitor) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Spec = desired.Spec
}
func mutatePodMonitor(existing, desired *monitoringv1.PodMonitor) {
existing.Annotations = desired.Annotations
existing.Labels = desired.Labels
existing.Spec = desired.Spec
}
func mutateService(existing, desired *corev1.Service) error {
existing.Spec.Ports = desired.Spec.Ports
if err := mergeWithOverride(&existing.Spec.Selector, desired.Spec.Selector); err != nil {
return err
}
return nil
}
func mutateDaemonset(existing, desired *appsv1.DaemonSet) error {
if !existing.CreationTimestamp.IsZero() && !apiequality.Semantic.DeepEqual(desired.Spec.Selector, existing.Spec.Selector) {
return ImmutableChangeErr
}
// Daemonset selector is immutable so we set this value only if
// a new object is going to be created
if existing.CreationTimestamp.IsZero() {
existing.Spec.Selector = desired.Spec.Selector
}
if err := mergeWithOverride(&existing.Spec, desired.Spec); err != nil {
return err
}
return nil
}
func mutateDeployment(existing, desired *appsv1.Deployment) error {
if !existing.CreationTimestamp.IsZero() && !apiequality.Semantic.DeepEqual(desired.Spec.Selector, existing.Spec.Selector) {
return ImmutableChangeErr
}
// Deployment selector is immutable so we set this value only if
// a new object is going to be created
if existing.CreationTimestamp.IsZero() {
existing.Spec.Selector = desired.Spec.Selector
}
existing.Spec.Replicas = desired.Spec.Replicas
if err := mergeWithOverride(&existing.Spec.Template, desired.Spec.Template); err != nil {
return err
}
if err := mergeWithOverride(&existing.Spec.Strategy, desired.Spec.Strategy); err != nil {
return err
}
return nil
}
func mutateStatefulSet(existing, desired *appsv1.StatefulSet) error {
if hasChange, field := hasImmutableFieldChange(existing, desired); hasChange {
return fmt.Errorf("%s is being changed, %w", field, ImmutableChangeErr)
}
// StatefulSet selector is immutable so we set this value only if
// a new object is going to be created
if existing.CreationTimestamp.IsZero() {
existing.Spec.Selector = desired.Spec.Selector
}
existing.Spec.PodManagementPolicy = desired.Spec.PodManagementPolicy
existing.Spec.Replicas = desired.Spec.Replicas
for i := range existing.Spec.VolumeClaimTemplates {
existing.Spec.VolumeClaimTemplates[i].TypeMeta = desired.Spec.VolumeClaimTemplates[i].TypeMeta
existing.Spec.VolumeClaimTemplates[i].ObjectMeta = desired.Spec.VolumeClaimTemplates[i].ObjectMeta
existing.Spec.VolumeClaimTemplates[i].Spec = desired.Spec.VolumeClaimTemplates[i].Spec
}
if err := mergeWithOverride(&existing.Spec.Template, desired.Spec.Template); err != nil {
return err
}
return nil
}
func hasImmutableFieldChange(existing, desired *appsv1.StatefulSet) (bool, string) {
if existing.CreationTimestamp.IsZero() {
return false, ""
}
if !apiequality.Semantic.DeepEqual(desired.Spec.Selector, existing.Spec.Selector) {
return true, fmt.Sprintf("Spec.Selector: desired: %s existing: %s", desired.Spec.Selector, existing.Spec.Selector)
}
if hasVolumeClaimsTemplatesChanged(existing, desired) {
return true, "Spec.VolumeClaimTemplates"
}
return false, ""
}
// hasVolumeClaimsTemplatesChanged if volume claims template change has been detected.
// We need to do this manually due to some fields being automatically filled by the API server
// and these needs to be excluded from the comparison to prevent false positives.
func hasVolumeClaimsTemplatesChanged(existing, desired *appsv1.StatefulSet) bool {
if len(desired.Spec.VolumeClaimTemplates) != len(existing.Spec.VolumeClaimTemplates) {
return true
}
for i := range desired.Spec.VolumeClaimTemplates {
// VolumeMode is automatically set by the API server, so if it is not set in the CR, assume it's the same as the existing one.
if desired.Spec.VolumeClaimTemplates[i].Spec.VolumeMode == nil || *desired.Spec.VolumeClaimTemplates[i].Spec.VolumeMode == "" {
desired.Spec.VolumeClaimTemplates[i].Spec.VolumeMode = existing.Spec.VolumeClaimTemplates[i].Spec.VolumeMode
}
if desired.Spec.VolumeClaimTemplates[i].Name != existing.Spec.VolumeClaimTemplates[i].Name {
return true
}
if !apiequality.Semantic.DeepEqual(desired.Spec.VolumeClaimTemplates[i].Annotations, existing.Spec.VolumeClaimTemplates[i].Annotations) {
return true
}
if !apiequality.Semantic.DeepEqual(desired.Spec.VolumeClaimTemplates[i].Spec, existing.Spec.VolumeClaimTemplates[i].Spec) {
return true
}
}
return false
}