Skip to content

Commit ad89904

Browse files
authored
Merge pull request #12940 from sbueringer/pr-machine-updating-phase
🌱 Implement Updating Machine phase
2 parents 6e15d77 + 5d67371 commit ad89904

File tree

8 files changed

+133
-3
lines changed

8 files changed

+133
-3
lines changed

api/core/v1beta1/conversion.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ func (src *Machine) ConvertTo(dstRaw conversion.Hub) error {
408408
// Recover other values.
409409
if ok {
410410
dst.Spec.MinReadySeconds = restored.Spec.MinReadySeconds
411+
// Restore the phase, this also means that any client using v1beta1 during a round-trip
412+
// won't be able to write the Phase field. But that's okay as the only client writing the Phase
413+
// field should be the Machine controller.
414+
dst.Status.Phase = restored.Status.Phase
411415
}
412416

413417
return nil
@@ -1653,6 +1657,13 @@ func Convert_v1beta2_MachineStatus_To_v1beta1_MachineStatus(in *clusterv1.Machin
16531657
if err := autoConvert_v1beta2_MachineStatus_To_v1beta1_MachineStatus(in, out, s); err != nil {
16541658
return err
16551659
}
1660+
1661+
// Convert v1beta2 Updating phase to v1beta1 Running as Updating did not exist in v1beta1.
1662+
// We don't have to support a round-trip as only the core CAPI controller should write the Phase field.
1663+
if out.Phase == "Updating" {
1664+
out.Phase = "Running"
1665+
}
1666+
16561667
if !reflect.DeepEqual(in.LastUpdated, metav1.Time{}) {
16571668
out.LastUpdated = ptr.To(in.LastUpdated)
16581669
}

api/core/v1beta1/conversion_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ func hubMachineSpec(in *clusterv1.MachineSpec, c randfill.Continue) {
514514

515515
func hubMachineStatus(in *clusterv1.MachineStatus, c randfill.Continue) {
516516
c.FillNoCustom(in)
517+
518+
in.Phase = []string{"Updating", "Running"}[c.Intn(2)]
519+
517520
// Drop empty structs with only omit empty fields.
518521
if in.Deprecated != nil {
519522
if in.Deprecated.V1Beta1 == nil || reflect.DeepEqual(in.Deprecated.V1Beta1, &clusterv1.MachineV1Beta1DeprecatedStatus{}) {

api/core/v1beta2/machine_phase_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const (
4545
// become a Kubernetes Node in a Ready state.
4646
MachinePhaseRunning = MachinePhase("Running")
4747

48+
// MachinePhaseUpdating is the Machine state when the Machine
49+
// is updating.
50+
MachinePhaseUpdating = MachinePhase("Updating")
51+
4852
// MachinePhaseDeleting is the Machine state when a delete
4953
// request has been sent to the API Server,
5054
// but its infrastructure has not yet been fully deleted.

api/core/v1beta2/machine_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ type MachineStatus struct {
570570

571571
// phase represents the current phase of machine actuation.
572572
// +optional
573-
// +kubebuilder:validation:Enum=Pending;Provisioning;Provisioned;Running;Deleting;Deleted;Failed;Unknown
573+
// +kubebuilder:validation:Enum=Pending;Provisioning;Provisioned;Running;Updating;Deleting;Deleted;Failed;Unknown
574574
Phase string `json:"phase,omitempty"`
575575

576576
// certificatesExpiryDate is the expiry date of the machine certificates.
@@ -728,6 +728,7 @@ func (m *MachineStatus) GetTypedPhase() MachinePhase {
728728
MachinePhaseProvisioning,
729729
MachinePhaseProvisioned,
730730
MachinePhaseRunning,
731+
MachinePhaseUpdating,
731732
MachinePhaseDeleting,
732733
MachinePhaseDeleted,
733734
MachinePhaseFailed:

config/crd/bases/cluster.x-k8s.io_machines.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controllers/machine/machine_controller_status.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,10 @@ func setMachinePhaseAndLastUpdated(_ context.Context, m *clusterv1.Machine) {
839839
m.Status.SetTypedPhase(clusterv1.MachinePhaseRunning)
840840
}
841841

842+
if conditions.IsTrue(m, clusterv1.MachineUpdatingCondition) {
843+
m.Status.SetTypedPhase(clusterv1.MachinePhaseUpdating)
844+
}
845+
842846
// Set the phase to "deleting" if the deletion timestamp is set.
843847
if !m.DeletionTimestamp.IsZero() {
844848
m.Status.SetTypedPhase(clusterv1.MachinePhaseDeleting)

internal/controllers/machine/machine_controller_status_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@ import (
2626
corev1 "k8s.io/api/core/v1"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29+
utilfeature "k8s.io/component-base/featuregate/testing"
2930
"k8s.io/utils/ptr"
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132

3233
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
34+
runtimev1 "sigs.k8s.io/cluster-api/api/runtime/v1beta2"
3335
"sigs.k8s.io/cluster-api/controllers/clustercache"
36+
"sigs.k8s.io/cluster-api/feature"
3437
"sigs.k8s.io/cluster-api/util"
38+
"sigs.k8s.io/cluster-api/util/annotations"
3539
"sigs.k8s.io/cluster-api/util/conditions"
3640
v1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1"
3741
"sigs.k8s.io/cluster-api/util/kubeconfig"
@@ -2519,6 +2523,104 @@ func TestReconcileMachinePhases(t *testing.T) {
25192523
}, 10*time.Second).Should(BeTrue())
25202524
})
25212525

2526+
t.Run("Should set `Updating` when Machine is in-place updating", func(t *testing.T) {
2527+
utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.InPlaceUpdates, true)
2528+
2529+
g := NewWithT(t)
2530+
2531+
ns, err := env.CreateNamespace(ctx, "test-reconcile-machine-phases")
2532+
g.Expect(err).ToNot(HaveOccurred())
2533+
defer func() {
2534+
g.Expect(env.Cleanup(ctx, ns)).To(Succeed())
2535+
}()
2536+
2537+
nodeProviderID := fmt.Sprintf("test://%s", util.RandomString(6))
2538+
2539+
cluster := defaultCluster.DeepCopy()
2540+
cluster.Namespace = ns.Name
2541+
2542+
bootstrapConfig := defaultBootstrap.DeepCopy()
2543+
bootstrapConfig.SetNamespace(ns.Name)
2544+
infraMachine := defaultInfra.DeepCopy()
2545+
infraMachine.SetNamespace(ns.Name)
2546+
g.Expect(unstructured.SetNestedField(infraMachine.Object, nodeProviderID, "spec", "providerID")).To(Succeed())
2547+
machine := defaultMachine.DeepCopy()
2548+
machine.Namespace = ns.Name
2549+
2550+
// Create Node.
2551+
node := &corev1.Node{
2552+
ObjectMeta: metav1.ObjectMeta{
2553+
GenerateName: "machine-test-node-",
2554+
},
2555+
Spec: corev1.NodeSpec{ProviderID: nodeProviderID},
2556+
}
2557+
g.Expect(env.Create(ctx, node)).To(Succeed())
2558+
defer func() {
2559+
g.Expect(env.Cleanup(ctx, node)).To(Succeed())
2560+
}()
2561+
2562+
g.Expect(env.Create(ctx, cluster)).To(Succeed())
2563+
defaultKubeconfigSecret = kubeconfig.GenerateSecret(cluster, kubeconfig.FromEnvTestConfig(env.Config, cluster))
2564+
g.Expect(env.Create(ctx, defaultKubeconfigSecret)).To(Succeed())
2565+
// Set InfrastructureReady to true so ClusterCache creates the clusterAccessor.
2566+
patch := client.MergeFrom(cluster.DeepCopy())
2567+
cluster.Status.Initialization.InfrastructureProvisioned = ptr.To(true)
2568+
g.Expect(env.Status().Patch(ctx, cluster, patch)).To(Succeed())
2569+
2570+
g.Expect(env.Create(ctx, bootstrapConfig)).To(Succeed())
2571+
g.Expect(env.Create(ctx, infraMachine)).To(Succeed())
2572+
// We have to subtract 2 seconds, because .status.lastUpdated does not contain milliseconds.
2573+
preUpdate := time.Now().Add(-2 * time.Second)
2574+
// Create and wait on machine to make sure caches sync and reconciliation triggers.
2575+
g.Expect(env.CreateAndWait(ctx, machine)).To(Succeed())
2576+
2577+
modifiedMachine := machine.DeepCopy()
2578+
// Set NodeRef.
2579+
machine.Status.NodeRef = clusterv1.MachineNodeReference{Name: node.Name}
2580+
g.Expect(env.Status().Patch(ctx, modifiedMachine, client.MergeFrom(machine))).To(Succeed())
2581+
2582+
// Set bootstrap ready.
2583+
modifiedBootstrapConfig := bootstrapConfig.DeepCopy()
2584+
g.Expect(unstructured.SetNestedField(modifiedBootstrapConfig.Object, true, "status", "initialization", "dataSecretCreated")).To(Succeed())
2585+
g.Expect(unstructured.SetNestedField(modifiedBootstrapConfig.Object, "secret-data", "status", "dataSecretName")).To(Succeed())
2586+
g.Expect(env.Status().Patch(ctx, modifiedBootstrapConfig, client.MergeFrom(bootstrapConfig))).To(Succeed())
2587+
2588+
// Set infra ready.
2589+
modifiedInfraMachine := infraMachine.DeepCopy()
2590+
g.Expect(unstructured.SetNestedField(modifiedInfraMachine.Object, true, "status", "initialization", "provisioned")).To(Succeed())
2591+
g.Expect(env.Status().Patch(ctx, modifiedInfraMachine, client.MergeFrom(infraMachine))).To(Succeed())
2592+
2593+
// Set annotations on Machine, BootstrapConfig and InfraMachine to trigger an in-place update.
2594+
orig := modifiedBootstrapConfig.DeepCopy()
2595+
annotations.AddAnnotations(modifiedBootstrapConfig, map[string]string{
2596+
clusterv1.UpdateInProgressAnnotation: "",
2597+
})
2598+
g.Expect(env.Patch(ctx, modifiedBootstrapConfig, client.MergeFrom(orig))).To(Succeed())
2599+
orig = modifiedInfraMachine.DeepCopy()
2600+
annotations.AddAnnotations(modifiedInfraMachine, map[string]string{
2601+
clusterv1.UpdateInProgressAnnotation: "",
2602+
})
2603+
g.Expect(env.Patch(ctx, modifiedInfraMachine, client.MergeFrom(orig))).To(Succeed())
2604+
origMachine := modifiedMachine.DeepCopy()
2605+
annotations.AddAnnotations(modifiedMachine, map[string]string{
2606+
runtimev1.PendingHooksAnnotation: "UpdateMachine",
2607+
clusterv1.UpdateInProgressAnnotation: "",
2608+
})
2609+
g.Expect(env.Patch(ctx, modifiedMachine, client.MergeFrom(origMachine))).To(Succeed())
2610+
2611+
// Wait until Machine was reconciled.
2612+
g.Eventually(func(g Gomega) bool {
2613+
if err := env.DirectAPIServerGet(ctx, client.ObjectKeyFromObject(machine), machine); err != nil {
2614+
return false
2615+
}
2616+
g.Expect(machine.Status.GetTypedPhase()).To(Equal(clusterv1.MachinePhaseUpdating))
2617+
// Verify that the LastUpdated timestamp was updated
2618+
g.Expect(machine.Status.LastUpdated.IsZero()).To(BeFalse())
2619+
g.Expect(machine.Status.LastUpdated.After(preUpdate)).To(BeTrue())
2620+
return true
2621+
}, 10*time.Second).Should(BeTrue())
2622+
})
2623+
25222624
t.Run("Should set `Provisioned` when there is a ProviderID and there is no Node", func(t *testing.T) {
25232625
g := NewWithT(t)
25242626

internal/controllers/machine/suite_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939
"sigs.k8s.io/cluster-api/api/core/v1beta2/index"
4040
"sigs.k8s.io/cluster-api/controllers/clustercache"
4141
"sigs.k8s.io/cluster-api/controllers/remote"
42+
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
43+
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
4244
"sigs.k8s.io/cluster-api/internal/test/envtest"
4345
)
4446

@@ -99,8 +101,10 @@ func TestMain(m *testing.M) {
99101
clusterCache.(interface{ DisablePrivateKeyGeneration() }).DisablePrivateKeyGeneration()
100102

101103
if err := (&Reconciler{
102-
Client: mgr.GetClient(),
103-
APIReader: mgr.GetAPIReader(),
104+
Client: mgr.GetClient(),
105+
APIReader: mgr.GetAPIReader(),
106+
// Just adding a minimal RuntimeClient to avoid panics in tests.
107+
RuntimeClient: fakeruntimeclient.NewRuntimeClientBuilder().WithCatalog(runtimecatalog.New()).Build(),
104108
ClusterCache: clusterCache,
105109
RemoteConditionsGracePeriod: 5 * time.Minute,
106110
AdditionalSyncMachineLabels: nil,

0 commit comments

Comments
 (0)