diff --git a/pkg/daemon/daemon.go b/pkg/daemon/daemon.go index ddb3c54e7d..a69f881e62 100644 --- a/pkg/daemon/daemon.go +++ b/pkg/daemon/daemon.go @@ -357,6 +357,7 @@ func (dn *Daemon) EnterDegradedState(err error) { // // If any of the object names are the same, they will be pointer-equal. type stateAndConfigs struct { + bootstrapping bool state string currentConfig *mcfgv1.MachineConfig pendingConfig *mcfgv1.MachineConfig @@ -364,14 +365,18 @@ type stateAndConfigs struct { } func (dn *Daemon) getStateAndConfigs(pendingConfigName string) (*stateAndConfigs, error) { - state, err := getNodeAnnotationExt(dn.kubeClient.CoreV1().Nodes(), dn.name, MachineConfigDaemonStateAnnotationKey, true) + _, err := os.Lstat(InitialNodeAnnotationsFilePath) + bootstrapping := false if err != nil { - return nil, err - } - // Temporary hack: the MCS used to not write the state=done annotation - // key. If it's unset, let's write it now. - if state == "" { - state = MachineConfigDaemonStateDone + if os.IsNotExist(err) { + // The node annotation file (laid down by the MCS) + // doesn't exist, we must not be bootstrapping + } else { + return nil, err + } + } else { + bootstrapping = true + glog.Infof("In bootstrap mode") } currentConfigName, err := getNodeAnnotation(dn.kubeClient.CoreV1().Nodes(), dn.name, CurrentMachineConfigAnnotationKey) @@ -386,6 +391,16 @@ func (dn *Daemon) getStateAndConfigs(pendingConfigName string) (*stateAndConfigs if err != nil { return nil, err } + state, err := getNodeAnnotationExt(dn.kubeClient.CoreV1().Nodes(), dn.name, MachineConfigDaemonStateAnnotationKey, true) + if err != nil { + return nil, err + } + // Temporary hack: the MCS used to not write the state=done annotation + // key. If it's unset, let's write it now. + if state == "" { + state = MachineConfigDaemonStateDone + } + var desiredConfig *mcfgv1.MachineConfig if currentConfigName == desiredConfigName { desiredConfig = currentConfig @@ -415,6 +430,7 @@ func (dn *Daemon) getStateAndConfigs(pendingConfigName string) (*stateAndConfigs } return &stateAndConfigs{ + bootstrapping: bootstrapping, currentConfig: currentConfig, pendingConfig: pendingConfig, desiredConfig: desiredConfig, @@ -540,6 +556,22 @@ func (dn *Daemon) CheckStateOnBoot() error { select {} } + if state.bootstrapping { + if !dn.checkOS(state.currentConfig.Spec.OSImageURL) { + glog.Infof("Bootstrap pivot required") + // This only returns on error + return dn.updateOSAndReboot(state.currentConfig) + } else { + glog.Infof("No bootstrap pivot required; unlinking bootstrap node annotations") + // Delete the bootstrap node annotations; the + // currentConfig's osImageURL should now be *truth*. + // In other words if it drifts somehow, we go degraded. + if err := os.Remove(InitialNodeAnnotationsFilePath); err != nil { + return errors.Wrapf(err, "Removing initial node annotations file") + } + } + } + // Validate the on-disk state against what we *expect*. // // In the case where we're booting a node for the first time, or the MCD @@ -742,10 +774,6 @@ func (dn *Daemon) completeUpdate(desiredConfigName string) error { // triggerUpdateWithMachineConfig starts the update. It queries the cluster for // the current and desired config if they weren't passed. func (dn *Daemon) triggerUpdateWithMachineConfig(currentConfig *mcfgv1.MachineConfig, desiredConfig *mcfgv1.MachineConfig) error { - if err := dn.nodeWriter.SetUpdateWorking(dn.kubeClient.CoreV1().Nodes(), dn.name); err != nil { - return err - } - if currentConfig == nil { ccAnnotation, err := getNodeAnnotation(dn.kubeClient.CoreV1().Nodes(), dn.name, CurrentMachineConfigAnnotationKey) if err != nil { diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index 2000be7cce..95408bbe45 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -63,41 +63,16 @@ func (dn *Daemon) writePendingState(desiredConfig *mcfgv1.MachineConfig) error { return replaceFileContentsAtomically(pathStateJSON, b) } -// update the node to the provided node configuration. -func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) error { +// updateOSAndReboot is the last step in an update(), and it can also +// be called as a special case for the "bootstrap pivot". +func (dn *Daemon) updateOSAndReboot(newConfig *mcfgv1.MachineConfig) error { var err error - oldConfigName := oldConfig.GetName() - newConfigName := newConfig.GetName() - glog.Infof("Checking reconcilable for config %v to %v", oldConfigName, newConfigName) - // make sure we can actually reconcile this state - reconcilableError := dn.reconcilable(oldConfig, newConfig) - - if reconcilableError != nil { - msg := fmt.Sprintf("Can't reconcile config %v with %v: %v", oldConfigName, newConfigName, *reconcilableError) - if dn.recorder != nil { - dn.recorder.Eventf(newConfig, corev1.EventTypeWarning, "FailedToReconcile", msg) - } - dn.logSystem(msg) - return fmt.Errorf("%s", msg) - } - - // update files on disk that need updating - if err = dn.updateFiles(oldConfig, newConfig); err != nil { - return err - } - if err = dn.updateOS(newConfig); err != nil { return err } - if err = dn.updateSSHKeys(newConfig.Spec.Config.Passwd.Users); err != nil { - return err - } - - // TODO: Change the logic to be clearer - // We need to skip draining of the node when we are running once - // and there is no cluster. + // Skip draining of the node when we're not cluster driven if dn.onceFrom == "" { glog.Info("Update prepared; draining the node") @@ -120,12 +95,49 @@ func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) error { glog.V(2).Infof("Node successfully drained") } + // reboot. this function shouldn't actually return. + return dn.reboot(fmt.Sprintf("Node will reboot into config %v", newConfig.GetName())) +} + +// update the node to the provided node configuration. +func (dn *Daemon) update(oldConfig, newConfig *mcfgv1.MachineConfig) error { + var err error + + if dn.nodeWriter != nil { + if err = dn.nodeWriter.SetUpdateWorking(dn.kubeClient.CoreV1().Nodes(), dn.name); err != nil { + return err + } + } + + oldConfigName := oldConfig.GetName() + newConfigName := newConfig.GetName() + glog.Infof("Checking reconcilable for config %v to %v", oldConfigName, newConfigName) + // make sure we can actually reconcile this state + reconcilableError := dn.reconcilable(oldConfig, newConfig) + + if reconcilableError != nil { + msg := fmt.Sprintf("Can't reconcile config %v with %v: %v", oldConfigName, newConfigName, *reconcilableError) + if dn.recorder != nil { + dn.recorder.Eventf(newConfig, corev1.EventTypeWarning, "FailedToReconcile", msg) + } + dn.logSystem(msg) + return fmt.Errorf("%s", msg) + } + + // update files on disk that need updating + if err = dn.updateFiles(oldConfig, newConfig); err != nil { + return err + } + + if err = dn.updateSSHKeys(newConfig.Spec.Config.Passwd.Users); err != nil { + return err + } + if err = dn.writePendingState(newConfig); err != nil { return errors.Wrapf(err, "writing pending state") } - // reboot. this function shouldn't actually return. - return dn.reboot(fmt.Sprintf("Node will reboot into config %v", newConfigName)) + return dn.updateOSAndReboot(newConfig) } // reconcilable checks the configs to make sure that the only changes requested