From b51f5e3d061b2b333ed2de8411d88af93b52024b Mon Sep 17 00:00:00 2001 From: Isabella Janssen Date: Fri, 14 Aug 2026 11:46:56 -0400 Subject: [PATCH 1/2] daemon: re-apply MC /etc files after OS update to fix ostree 3-way merge overwrites Co-authored-by: Claude Opus 4.6 --- pkg/daemon/daemon.go | 33 +++++++++++++++++++++++++++++++++ pkg/daemon/update.go | 17 +++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 2e835fa4b1..144b70fb65 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,34 @@ 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 { + 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") } From c6b101026973e55c127b0a0dde9faa4625ad27e7 Mon Sep 17 00:00:00 2001 From: Isabella Janssen Date: Mon, 17 Aug 2026 16:46:07 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=B0=20recommendation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/daemon/daemon.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index 144b70fb65..c08e502d5a 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -2283,7 +2283,9 @@ func (dn *Daemon) checkStateOnFirstRun() error { // 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 { + 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)