diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 2e835fa4b1..c08e502d5a 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -198,6 +198,11 @@ const ( // currentConfigPath is where we store the current config on disk to validate // against annotations changes currentConfigPath = "/etc/machine-config-daemon/currentconfig" + // postOSUpdateEtcFilesMarkerPath is written before a reboot that follows an + // OS update with MC-managed /etc files. On the next first-run it signals that + // those files must be re-applied, because the ostree 3-way merge may have + // overwritten them when their content matched the old OS base. + postOSUpdateEtcFilesMarkerPath = "/etc/machine-config-daemon/post-os-update-etc-files" // bootstrapConfigDiffPath is where we store the current config on disk to validate // against annotations changes bootstrapConfigDiffPath = "/etc/machine-config-daemon/bootstrapconfigdiff" @@ -2273,6 +2278,36 @@ func (dn *Daemon) checkStateOnFirstRun() error { return err } + // If a previous OS update left a marker, re-apply MC-managed /etc files + // before validation. The ostree 3-way merge silently overwrites MC-managed + // files whose content matches the old OS base, causing a spurious content + // mismatch. Re-applying here, after the merge has run, restores the correct + // content so validation passes. + if _, err := os.Stat(postOSUpdateEtcFilesMarkerPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("checking post-OS-update marker: %w", err) + } else if err == nil { + klog.Infof("Post-OS-update marker found; re-applying MC /etc files to correct 3-way merge overwrites") + if err := os.Remove(postOSUpdateEtcFilesMarkerPath); err != nil { + klog.Errorf("Failed to remove post-OS-update marker: %v", err) + } + newIgnConfig, err := ctrlcommon.ParseAndConvertConfig(state.currentConfig.Spec.Config.Raw) + if err != nil { + return fmt.Errorf("parsing current config for /etc re-apply after OS update: %w", err) + } + var etcFiles []ign3types.File + for _, f := range newIgnConfig.Storage.Files { + if strings.HasPrefix(f.Path, "/etc/") { + etcFiles = append(etcFiles, f) + } + } + if len(etcFiles) > 0 { + klog.Infof("Re-applying %d MC /etc files after OS update", len(etcFiles)) + if err := dn.writeFiles(etcFiles, false); err != nil { + return fmt.Errorf("re-applying /etc files after OS update: %w", err) + } + } + } + if err := dn.validateOnDiskStateOrImage(state.currentConfig, state.currentImage); err != nil { dn.nodeWriter.Eventf(corev1.EventTypeWarning, "OnDiskStateValidationFailed", err.Error()) // Start the config drift monitor even when there's pre-existing drift diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index c5af84523f..db7b8e294e 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -1302,6 +1302,23 @@ func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig, skipCertifi } } }() + var etcFiles []ign3types.File + for _, f := range newIgnConfig.Storage.Files { + if strings.HasPrefix(f.Path, "/etc/") { + etcFiles = append(etcFiles, f) + } + } + if osUpdate && len(etcFiles) > 0 { + // Signal checkStateOnFirstRun to re-apply these files after the reboot. + // The ostree 3-way /etc merge runs at boot time and can overwrite + // MC-managed files whose content matches the old OS base; writing to + // the staged deployment before the reboot would not survive the merge. + // The marker causes a post-reboot re-apply that corrects any overwrites. + if err := writeFileAtomicallyWithDefaults(postOSUpdateEtcFilesMarkerPath, []byte{}); err != nil { + return fmt.Errorf("writing post-OS-update marker: %w", err) + } + klog.Infof("Wrote post-OS-update marker; %d managed /etc files will be re-applied on next boot", len(etcFiles)) + } } else { klog.Info("updating the OS on non-CoreOS nodes is not supported") }