diff --git a/pkg/bundle/bundle.go b/pkg/bundle/bundle.go index 1b29a407..01396bbb 100644 --- a/pkg/bundle/bundle.go +++ b/pkg/bundle/bundle.go @@ -385,7 +385,7 @@ func (b *bundle) bundleTargetNamespaceSelector(bundleObj *trustapi.Bundle) (labe // created by the Update operation. func (b *bundle) migrateBundleStatusToApply(ctx context.Context, obj client.Object) (bool, error) { fieldManager := string(operator.FieldManager) - patch, err := csaupgrade.UpgradeManagedFieldsPatch(obj, sets.New(fieldManager, crRegressionFieldManager), fieldManager, csaupgrade.Subresource("status")) + patch, err := csaupgrade.UpgradeManagedFieldsPatch(obj, sets.New(fieldManager, ssa_client.RegressionFieldManager), fieldManager, csaupgrade.Subresource("status")) if err != nil { return false, err } diff --git a/pkg/bundle/internal/ssa_client/patch.go b/pkg/bundle/internal/ssa_client/patch.go index fe6a98bc..87e71349 100644 --- a/pkg/bundle/internal/ssa_client/patch.go +++ b/pkg/bundle/internal/ssa_client/patch.go @@ -21,6 +21,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + // RegressionFieldManager is the field manager that was introduced by a regression in controller-runtime + // version 0.15.0; fixed in 15.1 and 0.16.0: https://github.com/kubernetes-sigs/controller-runtime/pull/2435 + // trust-manager 0.6.0 was released with this regression in controller-runtime, which means that we have to + // take extra care when migrating from CSA to SSA. + RegressionFieldManager = "Go-http-client" +) + type applyPatch struct { patch []byte } diff --git a/pkg/bundle/internal/target/target.go b/pkg/bundle/internal/target/target.go index d3cf45b6..c9c3d45a 100644 --- a/pkg/bundle/internal/target/target.go +++ b/pkg/bundle/internal/target/target.go @@ -1,11 +1,19 @@ package target import ( + "bytes" "context" "fmt" trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" metav1applyconfig "k8s.io/client-go/applyconfigurations/meta/v1" + "k8s.io/client-go/util/csaupgrade" + "k8s.io/utils/ptr" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" + "strings" corev1 "k8s.io/api/core/v1" coreapplyconfig "k8s.io/client-go/applyconfigurations/core/v1" @@ -87,3 +95,117 @@ func NewSecretPatch(name types.NamespacedName, bundle trustapi.Bundle) *coreappl WithController(true), ) } + +type Kind string + +const ( + KindConfigMap Kind = "ConfigMap" + KindSecret Kind = "Secret" +) + +func (b *Reconciler) NeedsUpdate(ctx context.Context, kind Kind, log logr.Logger, obj *metav1.PartialObjectMetadata, bundle *trustapi.Bundle, dataHash string) (bool, error) { + needsUpdate := false + if !metav1.IsControlledBy(obj, bundle) { + needsUpdate = true + } + + if obj.GetLabels()[trustapi.BundleLabelKey] != bundle.Name { + needsUpdate = true + } + + if obj.GetAnnotations()[trustapi.BundleHashAnnotationKey] != dataHash { + needsUpdate = true + } + + { + var key string + var targetFieldNames []string + switch kind { + case KindConfigMap: + key = bundle.Spec.Target.ConfigMap.Key + targetFieldNames = []string{"data", "binaryData"} + case KindSecret: + key = bundle.Spec.Target.Secret.Key + targetFieldNames = []string{"data"} + default: + return false, fmt.Errorf("unknown targetType: %s", kind) + } + + properties, err := listManagedProperties(obj, string(operator.FieldManager), targetFieldNames...) + if err != nil { + return false, fmt.Errorf("failed to list managed properties: %w", err) + } + expectedProperties := sets.New[string](key) + if bundle.Spec.Target.AdditionalFormats != nil && bundle.Spec.Target.AdditionalFormats.JKS != nil { + expectedProperties.Insert(bundle.Spec.Target.AdditionalFormats.JKS.Key) + } + if bundle.Spec.Target.AdditionalFormats != nil && bundle.Spec.Target.AdditionalFormats.PKCS12 != nil { + expectedProperties.Insert(bundle.Spec.Target.AdditionalFormats.PKCS12.Key) + } + if !properties.Equal(expectedProperties) { + needsUpdate = true + } + + if kind == KindConfigMap { + if bundle.Spec.Target.ConfigMap != nil { + // Check if we need to migrate the ConfigMap managed fields to the Apply field operation + if didMigrate, err := b.migrateConfigMapToApply(ctx, obj); err != nil { + return false, fmt.Errorf("failed to migrate ConfigMap %s/%s to Apply: %w", obj.Namespace, obj.Name, err) + } else if didMigrate { + log.V(2).Info("migrated configmap from CSA to SSA") + needsUpdate = true + } + } + } + } + return needsUpdate, nil +} + +func listManagedProperties(configmap *metav1.PartialObjectMetadata, fieldManager string, fieldNames ...string) (sets.Set[string], error) { + properties := sets.New[string]() + + for _, managedField := range configmap.ManagedFields { + // If the managed field isn't owned by the cert-manager controller, ignore. + if managedField.Manager != fieldManager || managedField.FieldsV1 == nil { + continue + } + + // Decode the managed field. + var fieldset fieldpath.Set + if err := fieldset.FromJSON(bytes.NewReader(managedField.FieldsV1.Raw)); err != nil { + return nil, err + } + + for _, fieldName := range fieldNames { + // Extract the labels and annotations of the managed fields. + configmapData := fieldset.Children.Descend(fieldpath.PathElement{ + FieldName: ptr.To(fieldName), + }) + + // Gather the properties on the managed fields. Remove the '.' + // prefix which appears on managed field keys. + configmapData.Iterate(func(path fieldpath.Path) { + properties.Insert(strings.TrimPrefix(path.String(), ".")) + }) + } + } + + return properties, nil +} + +// MIGRATION: This is a migration function that migrates the ownership of +// fields from the Update operation to the Apply operation. This is required +// to ensure that the apply operations will also remove fields that were +// created by the Update operation. +func (b *Reconciler) migrateConfigMapToApply(ctx context.Context, obj client.Object) (bool, error) { + fieldManager := string(operator.FieldManager) + patch, err := csaupgrade.UpgradeManagedFieldsPatch(obj, sets.New(fieldManager, ssa_client.RegressionFieldManager), fieldManager) + if err != nil { + return false, err + } + if patch != nil { + return true, b.Client.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, patch)) + } + // No work to be done - already upgraded + return false, nil +} diff --git a/pkg/bundle/target.go b/pkg/bundle/target.go index b6050343..9c003ac6 100644 --- a/pkg/bundle/target.go +++ b/pkg/bundle/target.go @@ -17,34 +17,16 @@ limitations under the License. package bundle import ( - "bytes" "context" "crypto/sha256" "errors" "fmt" + trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" "github.com/cert-manager/trust-manager/pkg/bundle/internal/target" - "strings" - "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/util/csaupgrade" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/structured-merge-diff/fieldpath" - - trustapi "github.com/cert-manager/trust-manager/pkg/apis/trust/v1alpha1" - "github.com/cert-manager/trust-manager/pkg/operator" -) - -const ( - // crRegressionFieldManager is the field manager that was introduced by a regression in controller-runtime - // version 0.15.0; fixed in 15.1 and 0.16.0: https://github.com/kubernetes-sigs/controller-runtime/pull/2435 - // trust-manager 0.6.0 was released with this regression in controller-runtime, which means that we have to - // take extra care when migrating from CSA to SSA. - crRegressionFieldManager = "Go-http-client" ) // syncConfigMapTarget syncs the given data to the target ConfigMap in the given namespace. @@ -105,7 +87,7 @@ func (b *bundle) syncConfigMapTarget( // If the ConfigMap doesn't exist, create it. if !apierrors.IsNotFound(err) { // Exit early if no update is needed - if exit, err := b.needsUpdate(ctx, targetKindConfigMap, log, targetObj, bundle, dataHash); err != nil { + if exit, err := b.targetReconciler.NeedsUpdate(ctx, target.KindConfigMap, log, targetObj, bundle, dataHash); err != nil { return false, err } else if !exit { return false, nil @@ -189,7 +171,7 @@ func (b *bundle) syncSecretTarget( // If the Secret doesn't exist, create it. if !apierrors.IsNotFound(err) { // Exit early if no update is needed - if exit, err := b.needsUpdate(ctx, targetKindSecret, log, targetObj, bundle, dataHash); err != nil { + if exit, err := b.targetReconciler.NeedsUpdate(ctx, target.KindSecret, log, targetObj, bundle, dataHash); err != nil { return false, err } else if !exit { return false, nil @@ -210,117 +192,3 @@ func (b *bundle) syncSecretTarget( return true, nil } - -type targetKind string - -const ( - targetKindConfigMap targetKind = "ConfigMap" - targetKindSecret targetKind = "Secret" -) - -func (b *bundle) needsUpdate(ctx context.Context, kind targetKind, log logr.Logger, obj *metav1.PartialObjectMetadata, bundle *trustapi.Bundle, dataHash string) (bool, error) { - needsUpdate := false - if !metav1.IsControlledBy(obj, bundle) { - needsUpdate = true - } - - if obj.GetLabels()[trustapi.BundleLabelKey] != bundle.Name { - needsUpdate = true - } - - if obj.GetAnnotations()[trustapi.BundleHashAnnotationKey] != dataHash { - needsUpdate = true - } - - { - var key string - var targetFieldNames []string - switch kind { - case targetKindConfigMap: - key = bundle.Spec.Target.ConfigMap.Key - targetFieldNames = []string{"data", "binaryData"} - case targetKindSecret: - key = bundle.Spec.Target.Secret.Key - targetFieldNames = []string{"data"} - default: - return false, fmt.Errorf("unknown targetType: %s", kind) - } - - properties, err := listManagedProperties(obj, string(operator.FieldManager), targetFieldNames...) - if err != nil { - return false, fmt.Errorf("failed to list managed properties: %w", err) - } - expectedProperties := sets.New[string](key) - if bundle.Spec.Target.AdditionalFormats != nil && bundle.Spec.Target.AdditionalFormats.JKS != nil { - expectedProperties.Insert(bundle.Spec.Target.AdditionalFormats.JKS.Key) - } - if bundle.Spec.Target.AdditionalFormats != nil && bundle.Spec.Target.AdditionalFormats.PKCS12 != nil { - expectedProperties.Insert(bundle.Spec.Target.AdditionalFormats.PKCS12.Key) - } - if !properties.Equal(expectedProperties) { - needsUpdate = true - } - - if kind == targetKindConfigMap { - if bundle.Spec.Target.ConfigMap != nil { - // Check if we need to migrate the ConfigMap managed fields to the Apply field operation - if didMigrate, err := b.migrateConfigMapToApply(ctx, obj); err != nil { - return false, fmt.Errorf("failed to migrate ConfigMap %s/%s to Apply: %w", obj.Namespace, obj.Name, err) - } else if didMigrate { - log.V(2).Info("migrated configmap from CSA to SSA") - needsUpdate = true - } - } - } - } - return needsUpdate, nil -} - -func listManagedProperties(configmap *metav1.PartialObjectMetadata, fieldManager string, fieldNames ...string) (sets.Set[string], error) { - properties := sets.New[string]() - - for _, managedField := range configmap.ManagedFields { - // If the managed field isn't owned by the cert-manager controller, ignore. - if managedField.Manager != fieldManager || managedField.FieldsV1 == nil { - continue - } - - // Decode the managed field. - var fieldset fieldpath.Set - if err := fieldset.FromJSON(bytes.NewReader(managedField.FieldsV1.Raw)); err != nil { - return nil, err - } - - for _, fieldName := range fieldNames { - // Extract the labels and annotations of the managed fields. - configmapData := fieldset.Children.Descend(fieldpath.PathElement{ - FieldName: ptr.To(fieldName), - }) - - // Gather the properties on the managed fields. Remove the '.' - // prefix which appears on managed field keys. - configmapData.Iterate(func(path fieldpath.Path) { - properties.Insert(strings.TrimPrefix(path.String(), ".")) - }) - } - } - - return properties, nil -} - -// MIGRATION: This is a migration function that migrates the ownership of -// fields from the Update operation to the Apply operation. This is required -// to ensure that the apply operations will also remove fields that were -// created by the Update operation. -func (b *bundle) migrateConfigMapToApply(ctx context.Context, obj client.Object) (bool, error) { - fieldManager := string(operator.FieldManager) - patch, err := csaupgrade.UpgradeManagedFieldsPatch(obj, sets.New(fieldManager, crRegressionFieldManager), fieldManager) - if err != nil { - return false, err - } - if patch != nil { - return true, b.client.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, patch)) - } - // No work to be done - already upgraded - return false, nil -}