From e3402975dfb91240fb06dbbb22b125a915d0ecd1 Mon Sep 17 00:00:00 2001 From: Ondra Kupka Date: Fri, 7 Nov 2025 08:46:40 +0100 Subject: [PATCH] deps: Update library-go to update staticpod pkg This aligns the way installerpod and certsyncpod populate directories with secrets/configmaps. --- go.mod | 2 +- go.sum | 4 +- .../resourceapply/admissionregistration.go | 48 +++++++ .../resource/resourceapply/generic.go | 26 +++- .../resource/resourceapply/networking.go | 16 ++- .../resource/resourceapply/storage.go | 54 ++++++-- .../revisioncontroller/revision_controller.go | 16 +-- .../certsyncpod/certsync_controller.go | 130 ++++++++++-------- .../controller/guard/manifests/guard-pod.yaml | 1 + .../operator/staticpod/installerpod/cmd.go | 103 +++++++++----- .../internal/atomicdir/swap_linux.go | 22 +++ .../internal/atomicdir/swap_other.go | 12 ++ .../staticpod/internal/atomicdir/sync.go | 88 ++++++++++++ .../internal/atomicdir/types/file.go | 10 ++ vendor/modules.txt | 4 +- 15 files changed, 411 insertions(+), 125 deletions(-) create mode 100644 vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go create mode 100644 vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go create mode 100644 vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go create mode 100644 vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go diff --git a/go.mod b/go.mod index 58a92ce0f6..6df1a4ad81 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/openshift/api v0.0.0-20251015095338-264e80a2b6e7 github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 - github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 + github.com/openshift/library-go v0.0.0-20251106210235-69ca907a9c40 github.com/pkg/profile v1.7.0 // indirect github.com/prometheus/client_golang v1.22.0 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 88b9dd6227..de7b1f1067 100644 --- a/go.sum +++ b/go.sum @@ -166,8 +166,8 @@ github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+S github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY= github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM= -github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 h1:bANtDc8SgetSK4nQehf59x3+H9FqVJCprgjs49/OTg0= -github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= +github.com/openshift/library-go v0.0.0-20251106210235-69ca907a9c40 h1:mL7bq/DvJoS8F4gdRkvEITWKXcp8giAfBnHNSs5KXgA= +github.com/openshift/library-go v0.0.0-20251106210235-69ca907a9c40/go.mod h1:OlFFws1AO51uzfc48MsStGE4SFMWlMZD0+f5a/zCtKI= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go index 88bd00b251..0b52c3a324 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/admissionregistration.go @@ -415,3 +415,51 @@ func ApplyValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissi cache.UpdateCachedResourceMetadata(requiredOriginal, actual) return actual, true, nil } + +func DeleteValidatingAdmissionPolicyV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicy) (*admissionregistrationv1beta1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1beta1(ctx context.Context, client admissionregistrationclientv1beta1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPoliciesGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicy) (*admissionregistrationv1.ValidatingAdmissionPolicy, bool, error) { + err := client.ValidatingAdmissionPolicies().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} + +func DeleteValidatingAdmissionPolicyBindingV1(ctx context.Context, client admissionregistrationclientv1.ValidatingAdmissionPolicyBindingsGetter, recorder events.Recorder, required *admissionregistrationv1.ValidatingAdmissionPolicyBinding) (*admissionregistrationv1.ValidatingAdmissionPolicyBinding, bool, error) { + err := client.ValidatingAdmissionPolicyBindings().Delete(ctx, required.Name, metav1.DeleteOptions{}) + if err != nil && apierrors.IsNotFound(err) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + resourcehelper.ReportDeleteEvent(recorder, required, err) + return nil, true, nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go index 9105464bd0..58f49823f3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/generic.go @@ -147,7 +147,7 @@ func ApplyDirectly(ctx context.Context, clients *ClientHolder, recorder events.R if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") } else { - result.Result, result.Changed, result.Error = ApplyNetworkPolicy(ctx, clients.kubeClient.NetworkingV1(), recorder, t) + result.Result, result.Changed, result.Error = ApplyNetworkPolicy(ctx, clients.kubeClient.NetworkingV1(), recorder, t, cache) } case *rbacv1.ClusterRole: if clients.kubeClient == nil { @@ -380,6 +380,30 @@ func DeleteAll(ctx context.Context, clients *ClientHolder, recorder events.Recor } else { _, result.Changed, result.Error = DeleteValidatingWebhookConfiguration(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1beta1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1beta1(ctx, clients.kubeClient.AdmissionregistrationV1beta1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicy: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + if clients.kubeClient == nil { + result.Error = fmt.Errorf("missing kubeClient") + } else { + _, result.Changed, result.Error = DeleteValidatingAdmissionPolicyBindingV1(ctx, clients.kubeClient.AdmissionregistrationV1(), recorder, t) + } case *storagev1.CSIDriver: if clients.kubeClient == nil { result.Error = fmt.Errorf("missing kubeClient") diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go index 0a3df326e4..cc2de17ff3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/networking.go @@ -15,34 +15,44 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" ) -// ApplyClusterRole merges objectmeta, does not worry about anything else -func ApplyNetworkPolicy(ctx context.Context, client networkingclientv1.NetworkPoliciesGetter, recorder events.Recorder, required *networkingv1.NetworkPolicy) (*networkingv1.NetworkPolicy, bool, error) { +// ApplyNetworkPolicy merges objectmeta and requires spec +func ApplyNetworkPolicy(ctx context.Context, client networkingclientv1.NetworkPoliciesGetter, recorder events.Recorder, required *networkingv1.NetworkPolicy, cache ResourceCache) (*networkingv1.NetworkPolicy, bool, error) { existing, err := client.NetworkPolicies(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{}) if apierrors.IsNotFound(err) { requiredCopy := required.DeepCopy() actual, err := client.NetworkPolicies(required.Namespace).Create( ctx, resourcemerge.WithCleanLabelsAndAnnotations(requiredCopy).(*networkingv1.NetworkPolicy), metav1.CreateOptions{}) resourcehelper.ReportCreateEvent(recorder, required, err) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } if err != nil { return nil, false, err } + if cache.SafeToSkipApply(required, existing) { + return existing, false, nil + } + modified := false existingCopy := existing.DeepCopy() resourcemerge.EnsureObjectMeta(&modified, &existingCopy.ObjectMeta, required.ObjectMeta) - if equality.Semantic.DeepEqual(existingCopy.Spec, required.Spec) && !modified { + specContentSame := equality.Semantic.DeepEqual(existingCopy.Spec, required.Spec) + if specContentSame && !modified { + cache.UpdateCachedResourceMetadata(required, existingCopy) return existingCopy, false, nil } + existingCopy.Spec = required.Spec + if klog.V(2).Enabled() { klog.Infof("NetworkPolicy %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy)) } actual, err := client.NetworkPolicies(existingCopy.Namespace).Update(ctx, existingCopy, metav1.UpdateOptions{}) resourcehelper.ReportUpdateEvent(recorder, required, err) + cache.UpdateCachedResourceMetadata(required, actual) return actual, true, err } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go index d44a5d571a..afbdc53ee9 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/resource/resourceapply/storage.go @@ -135,9 +135,10 @@ func storageClassNeedsRecreate(oldSC, newSC *storagev1.StorageClass) bool { return false } -// ApplyCSIDriver merges objectmeta, does not worry about anything else +// ApplyCSIDriver merges objectmeta and tries to update spec if any of the required fields were cleared by the API server. +// It assumes they were cleared due to a feature gate not enabled in the API server and it will be enabled soon. +// When used by StaticResourceController, it will retry periodically and eventually save the spec with the field. func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter, recorder events.Recorder, requiredOriginal *storagev1.CSIDriver) (*storagev1.CSIDriver, bool, error) { - required := requiredOriginal.DeepCopy() if required.Annotations == nil { required.Annotations = map[string]string{} @@ -173,14 +174,40 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter } } - metadataModified := false + needsUpdate := false + // Most CSIDriver fields are immutable. Any change to them should trigger Delete() + Create() calls. + needsRecreate := false + existingCopy := existing.DeepCopy() - resourcemerge.EnsureObjectMeta(&metadataModified, &existingCopy.ObjectMeta, required.ObjectMeta) + // Metadata change should need just Update() call. + resourcemerge.EnsureObjectMeta(&needsUpdate, &existingCopy.ObjectMeta, required.ObjectMeta) requiredSpecHash := required.Annotations[specHashAnnotation] existingSpecHash := existing.Annotations[specHashAnnotation] - sameSpec := requiredSpecHash == existingSpecHash - if sameSpec && !metadataModified { + // Assume whole re-create is needed on any spec change. + // We don't keep a track of which field is mutable. + needsRecreate = requiredSpecHash != existingSpecHash + + // TODO: remove when CSIDriver spec.nodeAllocatableUpdatePeriodSeconds is enabled by default + // (https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/4876-mutable-csinode-allocatable) + if !needsRecreate && !alphaFieldsSaved(existingCopy, required) { + // The required spec is the same as in previous succesful call, however, + // the API server must have cleared some alpha/beta fields in it. + // Try to save the object again. In case the fields are cleared again, + // the caller (typically StaticResourceController) must retry periodically. + klog.V(4).Infof("Detected CSIDriver %q field cleared by the API server, updating", required.Name) + + // Assumption: the alpha fields are **mutable**, so only Update() is needed. + // Update() with the same spec as before + the field cleared by the API server + // won't generate any informer events. StaticResourceController will retry with + // periodic retry (1 minute.) + // We cannot use needsRecreate=true, as it will generate informer events and + // StaticResourceController will retry immediately, leading to a busy loop. + needsUpdate = true + existingCopy.Spec = required.Spec + } + + if !needsUpdate && !needsRecreate { return existing, false, nil } @@ -188,16 +215,16 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter klog.Infof("CSIDriver %q changes: %v", required.Name, JSONPatchNoError(existing, existingCopy)) } - if sameSpec { - // Update metadata by a simple Update call + if !needsRecreate { + // only needsUpdate is true, update the object by a simple Update call actual, err := client.CSIDrivers().Update(ctx, existingCopy, metav1.UpdateOptions{}) resourcehelper.ReportUpdateEvent(recorder, required, err) return actual, true, err } + // needsRecreate is true, needsUpdate does not matter. Delete and re-create the object. existingCopy.Spec = required.Spec existingCopy.ObjectMeta.ResourceVersion = "" - // Spec is read-only after creation. Delete and re-create the object err = client.CSIDrivers().Delete(ctx, existingCopy.Name, metav1.DeleteOptions{}) resourcehelper.ReportDeleteEvent(recorder, existingCopy, err, "Deleting CSIDriver to re-create it with updated parameters") if err != nil && !apierrors.IsNotFound(err) { @@ -214,10 +241,17 @@ func ApplyCSIDriver(ctx context.Context, client storageclientv1.CSIDriversGetter } else if err != nil { err = fmt.Errorf("failed to re-create CSIDriver %s: %s", existingCopy.Name, err) } - resourcehelper.ReportCreateEvent(recorder, existingCopy, err) + resourcehelper.ReportCreateEvent(recorder, actual, err) return actual, true, err } +// alphaFieldsSaved checks that all required fields in the CSIDriver required spec are present and equal in the actual spec. +func alphaFieldsSaved(actual, required *storagev1.CSIDriver) bool { + // DeepDerivative checks that all fields in "required" are present and equal in "actual" + // Fields not present in "required" are ignored. + return equality.Semantic.DeepDerivative(required.Spec, actual.Spec) +} + func validateRequiredCSIDriverLabels(required *storagev1.CSIDriver) error { supportsEphemeralVolumes := false for _, mode := range required.Spec.VolumeLifecycleModes { diff --git a/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go index 4d835dd1e2..ed2a96c3d2 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/revisioncontroller/revision_controller.go @@ -22,17 +22,17 @@ import ( "k8s.io/klog/v2" ) -// RevisionController is a controller that watches a set of configmaps and secrets and them against a revision snapshot -// of them. If the original resources changes, the revision counter is increased, stored in LatestAvailableRevision -// field of the operator config and new snapshots suffixed by the revision are created. +// RevisionController monitors configmaps and secrets, comparing them against their revision snapshots. +// When these resources change, it increments the revision counter, updates the LatestAvailableRevision +// field in the operator config, and creates new revision-suffixed snapshots. type RevisionController struct { controllerInstanceName string targetNamespace string - // configMaps is the list of configmaps that are directly copied.A different actor/controller modifies these. + // configMaps is the list of configmaps that are directly copied. A different actor/controller modifies these. // the first element should be the configmap that contains the static pod manifest configMaps []RevisionResource - // secrets is a list of secrets that are directly copied for the current values. A different actor/controller modifies these. + // secrets is a list of secrets that are directly copied for the current values. A different actor/controller modifies these. secrets []RevisionResource operatorClient v1helpers.OperatorClient @@ -97,7 +97,7 @@ func NewRevisionController( } // createRevisionIfNeeded takes care of creating content for the static pods to use. -// returns whether or not requeue and if an error happened when updating status. Normally it updates status itself. +// returns whether or not requeue and if an error happened when updating status. Normally it updates status itself. func (c RevisionController) createRevisionIfNeeded(ctx context.Context, recorder events.Recorder, currentLastAvailableRevision int32) error { isLatestRevisionCurrent, requiredIsNotFound, reason := c.isLatestRevisionCurrent(ctx, currentLastAvailableRevision) @@ -367,9 +367,9 @@ func (c RevisionController) sync(ctx context.Context, syncCtx factory.SyncContex } // If the operator status's latest available revision is not the same as the observed latest revision, update the operator. - // This needs to be done even if the revision precondition is not required because it ensures our operator status is + // This needs to be done even if the revision precondition is not met because it ensures our operator status is // correct for all consumers. - // This is what is going to allow us to move where the state is stored in authentications.openshift.io. + // This ensures the operator status accurately reflects the latest revision. latestObservedRevision, err := c.getLatestAvailableRevision(ctx) if err != nil { return err diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go index 111776d994..6911454be0 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/certsyncpod/certsync_controller.go @@ -2,10 +2,12 @@ package certsyncpod import ( "context" + "fmt" "os" "path/filepath" "reflect" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -17,17 +19,19 @@ import ( "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/events" - "github.com/openshift/library-go/pkg/operator/staticpod" "github.com/openshift/library-go/pkg/operator/staticpod/controller/installer" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir" ) +const stagingDirUID = "cert-sync" + type CertSyncController struct { destinationDir string namespace string configMaps []installer.UnrevisionedResource secrets []installer.UnrevisionedResource - configmapGetter corev1interface.ConfigMapInterface + configMapGetter corev1interface.ConfigMapInterface configMapLister v1.ConfigMapLister secretGetter corev1interface.SecretInterface secretLister v1.SecretLister @@ -42,10 +46,10 @@ func NewCertSyncController(targetDir, targetNamespace string, configmaps, secret secrets: secrets, eventRecorder: eventRecorder.WithComponentSuffix("cert-sync-controller"), - configmapGetter: kubeClient.CoreV1().ConfigMaps(targetNamespace), + configMapGetter: kubeClient.CoreV1().ConfigMaps(targetNamespace), configMapLister: informers.Core().V1().ConfigMaps().Lister(), - secretLister: informers.Core().V1().Secrets().Lister(), secretGetter: kubeClient.CoreV1().Secrets(targetNamespace), + secretLister: informers.Core().V1().Secrets().Lister(), } return factory.New(). @@ -60,15 +64,12 @@ func NewCertSyncController(targetDir, targetNamespace string, configmaps, secret ) } -func getConfigMapDir(targetDir, configMapName string) string { - return filepath.Join(targetDir, "configmaps", configMapName) -} - -func getSecretDir(targetDir, secretName string) string { - return filepath.Join(targetDir, "secrets", secretName) -} - func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncContext) error { + if err := os.RemoveAll(getStagingDir(c.destinationDir)); err != nil { + c.eventRecorder.Warningf("CertificateUpdateFailed", fmt.Sprintf("Failed to prune staging directory: %v", err)) + return err + } + errors := []error{} klog.Infof("Syncing configmaps: %v", c.configMaps) @@ -80,7 +81,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue case apierrors.IsNotFound(err) && cm.Optional: - configMapFile := getConfigMapDir(c.destinationDir, cm.Name) + configMapFile := getConfigMapTargetDir(c.destinationDir, cm.Name) if _, err := os.Stat(configMapFile); os.IsNotExist(err) { // if the configmap file does not exist, there is no work to do, so skip making any live check and just return. // if the configmap actually exists in the API, we'll eventually see it on the watch. @@ -88,7 +89,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte } // Check with the live call it is really missing - configMap, err = c.configmapGetter.Get(ctx, cm.Name, metav1.GetOptions{}) + configMap, err = c.configMapGetter.Get(ctx, cm.Name, metav1.GetOptions{}) if err == nil { klog.Infof("Caches are stale. They don't see configmap '%s/%s', yet it is present", configMap.Namespace, configMap.Name) // We will get re-queued when we observe the change @@ -113,9 +114,10 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - contentDir := getConfigMapDir(c.destinationDir, cm.Name) + contentDir := getConfigMapTargetDir(c.destinationDir, cm.Name) + stagingDir := getConfigMapStagingDir(c.destinationDir, cm.Name) - data := map[string]string{} + data := make(map[string]string, len(configMap.Data)) for filename := range configMap.Data { fullFilename := filepath.Join(contentDir, filename) @@ -138,7 +140,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte klog.V(2).Infof("Syncing updated configmap '%s/%s'.", configMap.Namespace, configMap.Name) // We need to do a live get here so we don't overwrite a newer file with one from a stale cache - configMap, err = c.configmapGetter.Get(ctx, configMap.Name, metav1.GetOptions{}) + configMap, err = c.configMapGetter.Get(ctx, configMap.Name, metav1.GetOptions{}) if err != nil { // Even if the error is not exists we will act on it when caches catch up c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed getting configmap: %s/%s: %v", c.namespace, cm.Name, err) @@ -152,27 +154,11 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err) - errors = append(errors, err) - continue - } - for filename, content := range configMap.Data { - fullFilename := filepath.Join(contentDir, filename) - // if the existing is the same, do nothing - if reflect.DeepEqual(data[fullFilename], content) { - continue - } - - klog.Infof("Writing configmap manifest %q ...", fullFilename) - if err := staticpod.WriteFileAtomic([]byte(content), 0644, fullFilename); err != nil { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for configmap: %s/%s: %v", configMap.Namespace, configMap.Name, err) - errors = append(errors, err) - continue - } + files := make(map[string][]byte, len(configMap.Data)) + for k, v := range configMap.Data { + files[k] = []byte(v) } - c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated configmap: %s/%s", configMap.Namespace, configMap.Name) + errors = append(errors, syncDirectory(c.eventRecorder, "configmap", configMap.ObjectMeta, contentDir, 0755, stagingDir, files, 0644)) } klog.Infof("Syncing secrets: %v", c.secrets) @@ -184,7 +170,7 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue case apierrors.IsNotFound(err) && s.Optional: - secretFile := getSecretDir(c.destinationDir, s.Name) + secretFile := getSecretTargetDir(c.destinationDir, s.Name) if _, err := os.Stat(secretFile); os.IsNotExist(err) { // if the secret file does not exist, there is no work to do, so skip making any live check and just return. // if the secret actually exists in the API, we'll eventually see it on the watch. @@ -218,9 +204,10 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - contentDir := getSecretDir(c.destinationDir, s.Name) + contentDir := getSecretTargetDir(c.destinationDir, s.Name) + stagingDir := getSecretStagingDir(c.destinationDir, s.Name) - data := map[string][]byte{} + data := make(map[string][]byte, len(secret.Data)) for filename := range secret.Data { fullFilename := filepath.Join(contentDir, filename) @@ -257,29 +244,50 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte continue } - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil && !os.IsExist(err) { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating directory for secret: %s/%s: %v", secret.Namespace, secret.Name, err) - errors = append(errors, err) - continue - } - for filename, content := range secret.Data { - // TODO fix permissions - fullFilename := filepath.Join(contentDir, filename) - // if the existing is the same, do nothing - if reflect.DeepEqual(data[fullFilename], content) { - continue - } + errors = append(errors, syncDirectory(c.eventRecorder, "secret", secret.ObjectMeta, contentDir, 0755, stagingDir, secret.Data, 0600)) + } + return utilerrors.NewAggregate(errors) +} - klog.Infof("Writing secret manifest %q ...", fullFilename) - if err := staticpod.WriteFileAtomic(content, 0600, fullFilename); err != nil { - c.eventRecorder.Warningf("CertificateUpdateFailed", "Failed writing file for secret: %s/%s: %v", secret.Namespace, secret.Name, err) - errors = append(errors, err) - continue - } +func syncDirectory( + eventRecorder events.Recorder, + typeName string, o metav1.ObjectMeta, + targetDir string, targetDirPerm os.FileMode, stagingDir string, + fileContents map[string][]byte, filePerm os.FileMode, +) error { + files := make(map[string]types.File, len(fileContents)) + for filename, content := range fileContents { + files[filename] = types.File{ + Content: content, + Perm: filePerm, } - c.eventRecorder.Eventf("CertificateUpdated", "Wrote updated secret: %s/%s", secret.Namespace, secret.Name) } - return utilerrors.NewAggregate(errors) + if err := atomicdir.Sync(targetDir, targetDirPerm, stagingDir, files); err != nil { + err = fmt.Errorf("failed to sync %s %s/%s (directory %q): %w", typeName, o.Name, o.Namespace, targetDir, err) + eventRecorder.Warning("CertificateUpdateFailed", err.Error()) + return err + } + eventRecorder.Eventf("CertificateUpdated", "Wrote updated %s: %s/%s", typeName, o.Namespace, o.Name) + return nil +} + +func getStagingDir(targetDir string) string { + return filepath.Join(targetDir, "staging", stagingDirUID) +} + +func getConfigMapTargetDir(targetDir, configMapName string) string { + return filepath.Join(targetDir, "configmaps", configMapName) +} + +func getConfigMapStagingDir(targetDir, configMapName string) string { + return filepath.Join(getStagingDir(targetDir), "configmaps", configMapName) +} + +func getSecretTargetDir(targetDir, secretName string) string { + return filepath.Join(targetDir, "secrets", secretName) +} + +func getSecretStagingDir(targetDir, secretName string) string { + return filepath.Join(getStagingDir(targetDir), "secrets", secretName) } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml index 657c87ec8b..29d4a345b3 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/controller/guard/manifests/guard-pod.yaml @@ -14,6 +14,7 @@ spec: terminationGracePeriodSeconds: 3 tolerations: - operator: Exists + hostUsers: false containers: - name: guard image: # Value set by operator diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go index 31afadff86..35424a56f5 100644 --- a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/installerpod/cmd.go @@ -5,38 +5,40 @@ import ( "fmt" "os" "path" + "path/filepath" "sort" "strconv" "strings" "time" - "k8s.io/utils/clock" - - "k8s.io/apimachinery/pkg/util/wait" - "github.com/blang/semver/v4" "github.com/davecgh/go-spew/spew" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" "github.com/spf13/cobra" "github.com/spf13/pflag" - "k8s.io/klog/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/server" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/klog/v2" + "k8s.io/utils/clock" "github.com/openshift/library-go/pkg/config/client" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourceread" "github.com/openshift/library-go/pkg/operator/resource/retry" - "github.com/openshift/library-go/pkg/operator/staticpod" "github.com/openshift/library-go/pkg/operator/staticpod/internal" + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir" "github.com/openshift/library-go/pkg/operator/staticpod/internal/flock" ) +const stagingDirUID = "installer" + type InstallOptions struct { // TODO replace with genericclioptions KubeConfig string @@ -219,8 +221,10 @@ func (o *InstallOptions) kubeletVersion(ctx context.Context) (string, error) { func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceDir string, secretNames, optionalSecretNames, configNames, optionalConfigNames sets.Set[string], prefixed bool) error { - klog.Infof("Creating target resource directory %q ...", resourceDir) - if err := os.MkdirAll(resourceDir, 0755); err != nil && !os.IsExist(err) { + + stagingDirBase := getStagingDir(resourceDir) + klog.Infof("Pruning staging directory %q ...", stagingDirBase) + if err := os.RemoveAll(stagingDirBase); err != nil { return err } @@ -258,34 +262,43 @@ func (o *InstallOptions) copySecretsAndConfigMaps(ctx context.Context, resourceD if prefixed { secretBaseName = o.prefixFor(secret.Name) } - contentDir := path.Join(resourceDir, "secrets", secretBaseName) - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil { - return err - } - for filename, content := range secret.Data { - if err := writeSecret(content, path.Join(contentDir, filename)); err != nil { - return err + + contentDir := getSecretTargetDir(resourceDir, secretBaseName) + stagingDir := getSecretStagingDir(resourceDir, secretBaseName) + + files := make(map[string]types.File, len(secret.Data)) + for name, content := range secret.Data { + files[name] = types.File{ + Content: content, + Perm: getFilePermissionsSecret(name), } } + + if err := atomicdir.Sync(contentDir, 0755, stagingDir, files); err != nil { + return fmt.Errorf("failed to sync secret %s/%s (directory %q): %w", secret.Namespace, secret.Name, contentDir, err) + } } for _, configmap := range configs { configMapBaseName := configmap.Name if prefixed { configMapBaseName = o.prefixFor(configmap.Name) } - contentDir := path.Join(resourceDir, "configmaps", configMapBaseName) - klog.Infof("Creating directory %q ...", contentDir) - if err := os.MkdirAll(contentDir, 0755); err != nil { - return err - } - for filename, content := range configmap.Data { - if err := writeConfig([]byte(content), path.Join(contentDir, filename)); err != nil { - return err + + contentDir := getConfigMapTargetDir(resourceDir, configMapBaseName) + stagingDir := getConfigMapStagingDir(resourceDir, configMapBaseName) + + files := make(map[string]types.File, len(configmap.Data)) + for name, content := range configmap.Data { + files[name] = types.File{ + Content: []byte(content), + Perm: getFilePermissionsConfigMap(name), } } - } + if err := atomicdir.Sync(contentDir, 0755, stagingDir, files); err != nil { + return fmt.Errorf("failed to sync configmap %s/%s (directory %q): %w", configmap.Namespace, configmap.Name, contentDir, err) + } + } return nil } @@ -626,22 +639,36 @@ func (o *InstallOptions) writePod(rawPodBytes []byte, manifestFileName, resource return nil } -func writeConfig(content []byte, fullFilename string) error { - klog.Infof("Writing config file %q ...", fullFilename) +func getStagingDir(targetDir string) string { + return filepath.Join(targetDir, "staging", stagingDirUID) +} - filePerms := os.FileMode(0600) - if strings.HasSuffix(fullFilename, ".sh") { - filePerms = 0755 - } - return staticpod.WriteFileAtomic(content, filePerms, fullFilename) +func getConfigMapTargetDir(targetDir, configMapName string) string { + return filepath.Join(targetDir, "configmaps", configMapName) } -func writeSecret(content []byte, fullFilename string) error { - klog.Infof("Writing secret manifest %q ...", fullFilename) +func getConfigMapStagingDir(targetDir, configMapName string) string { + return filepath.Join(getStagingDir(targetDir), "configmaps", configMapName) +} + +func getSecretTargetDir(targetDir, secretName string) string { + return filepath.Join(targetDir, "secrets", secretName) +} + +func getSecretStagingDir(targetDir, secretName string) string { + return filepath.Join(getStagingDir(targetDir), "secrets", secretName) +} + +func getFilePermissionsConfigMap(filename string) os.FileMode { + if strings.HasSuffix(filename, ".sh") { + return 0755 + } + return 0600 +} - filePerms := os.FileMode(0600) - if strings.HasSuffix(fullFilename, ".sh") { - filePerms = 0700 +func getFilePermissionsSecret(filename string) os.FileMode { + if strings.HasSuffix(filename, ".sh") { + return 0700 } - return staticpod.WriteFileAtomic(content, filePerms, fullFilename) + return 0600 } diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go new file mode 100644 index 0000000000..9ce912af76 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_linux.go @@ -0,0 +1,22 @@ +//go:build linux + +package atomicdir + +import ( + "golang.org/x/sys/unix" +) + +// swap can be used to exchange two directories atomically. +func swap(firstDir, secondDir string) error { + // Renameat2 can be used to exchange two directories atomically when RENAME_EXCHANGE flag is specified. + // The paths to be exchanged can be specified in multiple ways: + // + // * You can specify a file descriptor and a relative path to that descriptor. + // * You can specify an absolute path, in which case the file descriptor is ignored. + // + // We use AT_FDCWD special file descriptor so that when any of the paths is relative, + // it's relative to the current working directory. + // + // For more details, see `man renameat2` as that is the associated C library function. + return unix.Renameat2(unix.AT_FDCWD, firstDir, unix.AT_FDCWD, secondDir, unix.RENAME_EXCHANGE) +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go new file mode 100644 index 0000000000..6f5c0fed9e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/swap_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package atomicdir + +import "errors" + +// swap can be used to exchange two directories atomically. +// +// This function is only implemented for Linux and returns an error on other platforms. +func swap(firstDir, secondDir string) error { + return errors.New("swap is not supported on this platform") +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go new file mode 100644 index 0000000000..0c2cc3dfff --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/sync.go @@ -0,0 +1,88 @@ +package atomicdir + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types" +) + +// Sync can be used to atomically synchronize target directory with the given file content map. +// This is done by populating a staging directory, then atomically swapping it with the target directory. +// This effectively means that any extra files in the target directory are pruned. +// +// The staging directory needs to be explicitly specified. It is initially created using os.MkdirAll with targetDirPerm. +// It is then populated using files with filePerm. Once the atomic swap is performed, the staging directory +// (which is now the original target directory) is removed. +func Sync(targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string]types.File) error { + return sync(&realFS, targetDir, targetDirPerm, stagingDir, files) +} + +type fileSystem struct { + MkdirAll func(path string, perm os.FileMode) error + RemoveAll func(path string) error + WriteFile func(name string, data []byte, perm os.FileMode) error + SwapDirectories func(dirA, dirB string) error +} + +var realFS = fileSystem{ + MkdirAll: os.MkdirAll, + RemoveAll: os.RemoveAll, + WriteFile: os.WriteFile, + SwapDirectories: swap, +} + +// sync prepares a tmp directory and writes all files into that directory. +// Then it atomically swap the tmp directory for the target one. +// This is currently implemented as really atomically swapping directories. +// +// The same goal of atomic swap could be implemented using symlinks much like AtomicWriter does in +// https://github.com/kubernetes/kubernetes/blob/v1.34.0/pkg/volume/util/atomic_writer.go#L58 +// The reason we don't do that is that we already have a directory populated and watched that needs to we swapped. +// In other words, it's for compatibility reasons. And if we were to migrate to the symlink approach, +// we would anyway need to atomically turn the current data directory into a symlink. +// This would all just increase complexity and require atomic swap on the OS level anyway. +func sync(fs *fileSystem, targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string]types.File) (retErr error) { + klog.Infof("Ensuring target directory %q exists ...", targetDir) + if err := fs.MkdirAll(targetDir, targetDirPerm); err != nil { + return fmt.Errorf("failed creating target directory: %w", err) + } + + klog.Infof("Creating staging directory %q ...", stagingDir) + if err := fs.MkdirAll(stagingDir, targetDirPerm); err != nil { + return fmt.Errorf("failed creating staging directory: %w", err) + } + defer func() { + if err := fs.RemoveAll(stagingDir); err != nil { + if retErr != nil { + retErr = fmt.Errorf("failed removing staging directory %q: %w; previous error: %w", stagingDir, err, retErr) + return + } + retErr = fmt.Errorf("failed removing staging directory %q: %w", stagingDir, err) + } + }() + + for filename, file := range files { + // Make sure filename is a plain filename, not a path. + // This also ensures the staging directory cannot be escaped. + if filename != filepath.Base(filename) { + return fmt.Errorf("filename cannot be a path: %q", filename) + } + + fullFilename := filepath.Join(stagingDir, filename) + klog.Infof("Writing file %q ...", fullFilename) + + if err := fs.WriteFile(fullFilename, file.Content, file.Perm); err != nil { + return fmt.Errorf("failed writing %q: %w", fullFilename, err) + } + } + + klog.Infof("Atomically swapping staging directory %q with target directory %q ...", stagingDir, targetDir) + if err := fs.SwapDirectories(targetDir, stagingDir); err != nil { + return fmt.Errorf("failed swapping target directory %q with staging directory %q: %w", targetDir, stagingDir, err) + } + return +} diff --git a/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go new file mode 100644 index 0000000000..f99f41c57c --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types/file.go @@ -0,0 +1,10 @@ +// Package types exists to avoid import cycles as it's imported by both atomicdir and atomicdir/testing. +package types + +import "os" + +// File represents file content together with the associated permissions. +type File struct { + Content []byte + Perm os.FileMode +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7ca5a876b7..5e68d4e3b9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -349,7 +349,7 @@ github.com/openshift/client-go/security/informers/externalversions/internalinter github.com/openshift/client-go/security/informers/externalversions/security github.com/openshift/client-go/security/informers/externalversions/security/v1 github.com/openshift/client-go/security/listers/security/v1 -# github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5 +# github.com/openshift/library-go v0.0.0-20251106210235-69ca907a9c40 ## explicit; go 1.24.0 github.com/openshift/library-go/pkg/apiserver/jsonpatch github.com/openshift/library-go/pkg/assets @@ -424,6 +424,8 @@ github.com/openshift/library-go/pkg/operator/staticpod/controller/staticpodfallb github.com/openshift/library-go/pkg/operator/staticpod/controller/staticpodstate github.com/openshift/library-go/pkg/operator/staticpod/installerpod github.com/openshift/library-go/pkg/operator/staticpod/internal +github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir +github.com/openshift/library-go/pkg/operator/staticpod/internal/atomicdir/types github.com/openshift/library-go/pkg/operator/staticpod/internal/flock github.com/openshift/library-go/pkg/operator/staticpod/prune github.com/openshift/library-go/pkg/operator/staticpod/startupmonitor