Skip to content

Commit 4178ca8

Browse files
committed
MD: Implement CanUpdateMachineSet
Signed-off-by: Stefan Büringer [email protected]
1 parent 7445712 commit 4178ca8

18 files changed

+557
-181
lines changed

api/runtime/hooks/v1alpha1/inplaceupdate_types.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,17 @@ type CanUpdateMachineResponse struct {
6767
CommonResponse `json:",inline"`
6868

6969
// machinePatch when applied to the current Machine spec, indicates changes handled in-place.
70+
// Only fields in spec have to be covered by the patch.
7071
// +optional
7172
MachinePatch Patch `json:"machinePatch,omitempty,omitzero"`
7273

7374
// infrastructureMachinePatch indicates infra Machine spec changes handled in-place.
75+
// Only fields in spec have to be covered by the patch.
7476
// +optional
7577
InfrastructureMachinePatch Patch `json:"infrastructureMachinePatch,omitempty,omitzero"`
7678

7779
// bootstrapConfigPatch indicates bootstrap config spec changes handled in-place.
80+
// Only fields in spec have to be covered by the patch.
7881
// +optional
7982
BootstrapConfigPatch Patch `json:"bootstrapConfigPatch,omitempty,omitzero"`
8083
}
@@ -119,14 +122,17 @@ type CanUpdateMachineSetRequest struct {
119122
// CanUpdateMachineSetRequestObjects groups objects for CanUpdateMachineSetRequest.
120123
type CanUpdateMachineSetRequestObjects struct {
121124
// machineSet is the full MachineSet object.
125+
// Only fields in spec.template.spec have to be covered by the patch.
122126
// +required
123127
MachineSet clusterv1.MachineSet `json:"machineSet,omitempty,omitzero"`
124128

125129
// infrastructureMachineTemplate is the provider-specific InfrastructureMachineTemplate object.
130+
// Only fields in spec.template.spec have to be covered by the patch.
126131
// +required
127132
InfrastructureMachineTemplate runtime.RawExtension `json:"infrastructureMachineTemplate,omitempty,omitzero"`
128133

129134
// bootstrapConfigTemplate is the provider-specific BootstrapConfigTemplate object.
135+
// Only fields in spec.template.spec have to be covered by the patch.
130136
// +optional
131137
BootstrapConfigTemplate runtime.RawExtension `json:"bootstrapConfigTemplate,omitempty,omitzero"`
132138
}
@@ -217,7 +223,8 @@ func init() {
217223
"Notes:\n" +
218224
"- This hook is called during the planning phase of updates\n" +
219225
"- Only spec is provided, status fields are not included\n" +
220-
"- If no extension can cover the required changes, CAPI will fallback to rolling updates\n",
226+
"- If no extension can cover the required changes, CAPI will fallback to rolling updates\n" +
227+
"- Only fields in Machine/InfraMachine/BootstrapConfig spec have to be covered by patches\n",
221228
})
222229

223230
catalogBuilder.RegisterHook(CanUpdateMachineSet, &runtimecatalog.HookMeta{
@@ -230,7 +237,8 @@ func init() {
230237
"Notes:\n" +
231238
"- This hook is called during the planning phase of updates\n" +
232239
"- Only spec is provided, status fields are not included\n" +
233-
"- If no extension can cover the required changes, CAPI will fallback to rolling updates\n",
240+
"- If no extension can cover the required changes, CAPI will fallback to rolling updates\n" +
241+
"- Only fields in MachineSet/InfraMachineTemplate/BootstrapConfigTemplate spec.template.spec have to be covered by patches\n",
234242
})
235243

236244
catalogBuilder.RegisterHook(UpdateMachine, &runtimecatalog.HookMeta{

api/runtime/hooks/v1alpha1/zz_generated.openapi.go

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

controllers/alias.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ func (r *MachineSetReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma
123123

124124
// MachineDeploymentReconciler reconciles a MachineDeployment object.
125125
type MachineDeploymentReconciler struct {
126-
Client client.Client
127-
APIReader client.Reader
126+
Client client.Client
127+
APIReader client.Reader
128+
RuntimeClient runtimeclient.Client
128129

129130
// WatchFilterValue is the label value used to filter events prior to reconciliation.
130131
WatchFilterValue string
@@ -134,6 +135,7 @@ func (r *MachineDeploymentReconciler) SetupWithManager(ctx context.Context, mgr
134135
return (&machinedeploymentcontroller.Reconciler{
135136
Client: r.Client,
136137
APIReader: r.APIReader,
138+
RuntimeClient: r.RuntimeClient,
137139
WatchFilterValue: r.WatchFilterValue,
138140
}).SetupWithManager(ctx, mgr, options)
139141
}

controlplane/kubeadm/internal/controllers/inplace_canupdatemachine.go

Lines changed: 9 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20-
"bytes"
2120
"context"
22-
"encoding/json"
2321
"fmt"
24-
"runtime/debug"
2522
"strings"
2623

27-
jsonpatch "github.com/evanphx/json-patch/v5"
2824
"github.com/pkg/errors"
2925
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3026
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3127
"k8s.io/apimachinery/pkg/runtime"
32-
kerrors "k8s.io/apimachinery/pkg/util/errors"
3328
"k8s.io/klog/v2"
3429
ctrl "sigs.k8s.io/controller-runtime"
3530
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -40,7 +35,7 @@ import (
4035
"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
4136
"sigs.k8s.io/cluster-api/feature"
4237
"sigs.k8s.io/cluster-api/internal/util/compare"
43-
patchutil "sigs.k8s.io/cluster-api/internal/util/patch"
38+
"sigs.k8s.io/cluster-api/internal/util/patch"
4439
"sigs.k8s.io/cluster-api/internal/util/ssa"
4540
)
4641

@@ -75,7 +70,7 @@ func (r *KubeadmControlPlaneReconciler) canUpdateMachine(ctx context.Context, ma
7570
return false, nil
7671
}
7772
if len(extensionHandlers) > 1 {
78-
return false, errors.Errorf("found multiple CanUpdateMachine hooks (%s) (more than one is not supported yet)", strings.Join(extensionHandlers, ","))
73+
return false, errors.Errorf("found multiple CanUpdateMachine hooks (%s): only one hook is supported", strings.Join(extensionHandlers, ","))
7974
}
8075

8176
canUpdateMachine, reasons, err := r.canExtensionsUpdateMachine(ctx, machine, machineUpToDateResult, extensionHandlers)
@@ -186,19 +181,19 @@ func createRequest(ctx context.Context, c client.Client, currentMachine *cluster
186181
},
187182
}
188183
var err error
189-
req.Current.BootstrapConfig, err = convertToRawExtension(cleanupKubeadmConfig(currentKubeadmConfigForDiff))
184+
req.Current.BootstrapConfig, err = patch.ConvertToRawExtension(cleanupKubeadmConfig(currentKubeadmConfigForDiff))
190185
if err != nil {
191186
return nil, err
192187
}
193-
req.Desired.BootstrapConfig, err = convertToRawExtension(cleanupKubeadmConfig(desiredKubeadmConfigForDiff))
188+
req.Desired.BootstrapConfig, err = patch.ConvertToRawExtension(cleanupKubeadmConfig(desiredKubeadmConfigForDiff))
194189
if err != nil {
195190
return nil, err
196191
}
197-
req.Current.InfrastructureMachine, err = convertToRawExtension(cleanupUnstructured(currentInfraMachineForDiff))
192+
req.Current.InfrastructureMachine, err = patch.ConvertToRawExtension(cleanupUnstructured(currentInfraMachineForDiff))
198193
if err != nil {
199194
return nil, err
200195
}
201-
req.Desired.InfrastructureMachine, err = convertToRawExtension(cleanupUnstructured(desiredInfraMachineForDiff))
196+
req.Desired.InfrastructureMachine, err = patch.ConvertToRawExtension(cleanupUnstructured(desiredInfraMachineForDiff))
202197
if err != nil {
203198
return nil, err
204199
}
@@ -255,143 +250,28 @@ func cleanupUnstructured(u *unstructured.Unstructured) *unstructured.Unstructure
255250
return cleanedUpU
256251
}
257252

258-
func convertToRawExtension(object runtime.Object) (runtime.RawExtension, error) {
259-
objectBytes, err := json.Marshal(object)
260-
if err != nil {
261-
return runtime.RawExtension{}, errors.Wrap(err, "failed to marshal object to JSON")
262-
}
263-
264-
objectUnstructured, ok := object.(*unstructured.Unstructured)
265-
if !ok {
266-
objectUnstructured = &unstructured.Unstructured{}
267-
// Note: This only succeeds if object has apiVersion & kind set (which is always the case).
268-
if err := json.Unmarshal(objectBytes, objectUnstructured); err != nil {
269-
return runtime.RawExtension{}, errors.Wrap(err, "failed to Unmarshal object into Unstructured")
270-
}
271-
}
272-
273-
// Note: Raw and Object are always both set and Object is always set as an Unstructured
274-
// to simplify subsequent code in matchesUnstructuredSpec.
275-
return runtime.RawExtension{
276-
Raw: objectBytes,
277-
Object: objectUnstructured,
278-
}, nil
279-
}
280-
281253
func applyPatchesToRequest(ctx context.Context, req *runtimehooksv1.CanUpdateMachineRequest, resp *runtimehooksv1.CanUpdateMachineResponse) error {
282254
if resp.MachinePatch.IsDefined() {
283-
if err := applyPatchToMachine(ctx, &req.Current.Machine, resp.MachinePatch); err != nil {
255+
if err := patch.ApplyPatchToTypedObject(ctx, &req.Current.Machine, resp.MachinePatch, "spec"); err != nil {
284256
return err
285257
}
286258
}
287259

288260
if resp.BootstrapConfigPatch.IsDefined() {
289-
if _, err := applyPatchToObject(ctx, &req.Current.BootstrapConfig, resp.BootstrapConfigPatch); err != nil {
261+
if _, err := patch.ApplyPatchToObject(ctx, &req.Current.BootstrapConfig, resp.BootstrapConfigPatch, "spec"); err != nil {
290262
return err
291263
}
292264
}
293265

294266
if resp.InfrastructureMachinePatch.IsDefined() {
295-
if _, err := applyPatchToObject(ctx, &req.Current.InfrastructureMachine, resp.InfrastructureMachinePatch); err != nil {
267+
if _, err := patch.ApplyPatchToObject(ctx, &req.Current.InfrastructureMachine, resp.InfrastructureMachinePatch, "spec"); err != nil {
296268
return err
297269
}
298270
}
299271

300272
return nil
301273
}
302274

303-
func applyPatchToMachine(ctx context.Context, currentMachine *clusterv1.Machine, machinePath runtimehooksv1.Patch) error {
304-
// Note: Machine needs special handling because it is not a runtime.RawExtension. Simply converting it here to
305-
// a runtime.RawExtension so we can avoid making the code in applyPatchToObject more complex.
306-
currentMachineRaw, err := convertToRawExtension(currentMachine)
307-
if err != nil {
308-
return err
309-
}
310-
311-
machineChanged, err := applyPatchToObject(ctx, &currentMachineRaw, machinePath)
312-
if err != nil {
313-
return err
314-
}
315-
316-
if !machineChanged {
317-
return nil
318-
}
319-
320-
// Note: json.Unmarshal can't be used directly on *currentMachine as json.Unmarshal does not unset fields.
321-
patchedCurrentMachine := &clusterv1.Machine{}
322-
if err := json.Unmarshal(currentMachineRaw.Raw, patchedCurrentMachine); err != nil {
323-
return err
324-
}
325-
*currentMachine = *patchedCurrentMachine
326-
return nil
327-
}
328-
329-
// applyPatchToObject applies the patch to the obj.
330-
// Note: This is following the same general structure that is used in the applyPatchToRequest func in
331-
// internal/controllers/topology/cluster/patches/engine.go.
332-
func applyPatchToObject(ctx context.Context, obj *runtime.RawExtension, patch runtimehooksv1.Patch) (objChanged bool, reterr error) {
333-
log := ctrl.LoggerFrom(ctx)
334-
335-
if patch.PatchType == "" {
336-
return false, errors.Errorf("failed to apply patch: patchType is not set")
337-
}
338-
339-
defer func() {
340-
if r := recover(); r != nil {
341-
log.Info(fmt.Sprintf("Observed a panic when applying patch: %v\n%s", r, string(debug.Stack())))
342-
reterr = kerrors.NewAggregate([]error{reterr, fmt.Errorf("observed a panic when applying patch: %v", r)})
343-
}
344-
}()
345-
346-
// Create a copy of obj.Raw.
347-
// The patches will be applied to the copy and then only spec changes will be copied back to the obj.
348-
patchedObject := bytes.Clone(obj.Raw)
349-
var err error
350-
351-
switch patch.PatchType {
352-
case runtimehooksv1.JSONPatchType:
353-
log.V(5).Info("Accumulating JSON patch", "patch", string(patch.Patch))
354-
jsonPatch, err := jsonpatch.DecodePatch(patch.Patch)
355-
if err != nil {
356-
log.Error(err, "Failed to apply patch: error decoding json patch (RFC6902)", "patch", string(patch.Patch))
357-
return false, errors.Wrap(err, "failed to apply patch: error decoding json patch (RFC6902)")
358-
}
359-
360-
if len(jsonPatch) == 0 {
361-
// Return if there are no patches, nothing to do.
362-
return false, nil
363-
}
364-
365-
patchedObject, err = jsonPatch.Apply(patchedObject)
366-
if err != nil {
367-
log.Error(err, "Failed to apply patch: error applying json patch (RFC6902)", "patch", string(patch.Patch))
368-
return false, errors.Wrap(err, "failed to apply patch: error applying json patch (RFC6902)")
369-
}
370-
case runtimehooksv1.JSONMergePatchType:
371-
if len(patch.Patch) == 0 || bytes.Equal(patch.Patch, []byte("{}")) {
372-
// Return if there are no patches, nothing to do.
373-
return false, nil
374-
}
375-
376-
log.V(5).Info("Accumulating JSON merge patch", "patch", string(patch.Patch))
377-
patchedObject, err = jsonpatch.MergePatch(patchedObject, patch.Patch)
378-
if err != nil {
379-
log.Error(err, "Failed to apply patch: error applying json merge patch (RFC7386)", "patch", string(patch.Patch))
380-
return false, errors.Wrap(err, "failed to apply patch: error applying json merge patch (RFC7386)")
381-
}
382-
default:
383-
return false, errors.Errorf("failed to apply patch: unknown patchType %s", patch.PatchType)
384-
}
385-
386-
// Overwrite the spec of obj with the spec of the patchedObject,
387-
// to ensure that we only pick up changes to the spec.
388-
if err := patchutil.PatchSpec(obj, patchedObject); err != nil {
389-
return false, errors.Wrap(err, "failed to apply patch to object")
390-
}
391-
392-
return true, nil
393-
}
394-
395275
func matchesMachine(req *runtimehooksv1.CanUpdateMachineRequest) (bool, []string, error) {
396276
var reasons []string
397277
match, diff, err := matchesMachineSpec(&req.Current.Machine, &req.Desired.Machine)

controlplane/kubeadm/internal/controllers/inplace_canupdatemachine_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"sigs.k8s.io/cluster-api/feature"
4343
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
4444
"sigs.k8s.io/cluster-api/internal/util/compare"
45+
"sigs.k8s.io/cluster-api/internal/util/patch"
4546
"sigs.k8s.io/cluster-api/internal/util/ssa"
4647
"sigs.k8s.io/cluster-api/util/test/builder"
4748
)
@@ -104,7 +105,7 @@ func Test_canUpdateMachine(t *testing.T) {
104105
canUpdateMachineGVH: {"test-update-extension-1", "test-update-extension-2"},
105106
},
106107
wantError: true,
107-
wantErrorMessage: "found multiple CanUpdateMachine hooks (test-update-extension-1,test-update-extension-2) (more than one is not supported yet)",
108+
wantErrorMessage: "found multiple CanUpdateMachine hooks (test-update-extension-1,test-update-extension-2): only one hook is supported",
108109
},
109110
{
110111
name: "Return false if canExtensionsUpdateMachine returns false",
@@ -1174,7 +1175,7 @@ func diff(a, b any) string {
11741175
}
11751176

11761177
func mustConvertToRawExtension(object runtime.Object) runtime.RawExtension {
1177-
raw, err := convertToRawExtension(object)
1178+
raw, err := patch.ConvertToRawExtension(object)
11781179
if err != nil {
11791180
panic(err)
11801181
}

internal/controllers/machine/machine_controller_inplace_update.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package machine
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223
"time"
2324

2425
"github.com/pkg/errors"
@@ -149,7 +150,7 @@ func (r *Reconciler) callUpdateMachineHook(ctx context.Context, s *scope) (ctrl.
149150
}
150151

151152
if len(extensions) > 1 {
152-
return ctrl.Result{}, "", errors.Errorf("multiple extensions registered for UpdateMachine hook: only one extension is supported, found %d extensions: %v", len(extensions), extensions)
153+
return ctrl.Result{}, "", errors.Errorf("found multiple UpdateMachine hooks (%s): only one hook is supported", strings.Join(extensions, ","))
153154
}
154155

155156
// Note: When building request message, dropping status; Runtime extension should treat UpdateMachine

internal/controllers/machine/machine_controller_inplace_update_test.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,8 @@ func TestCallUpdateMachineHook(t *testing.T) {
367367
Build()
368368
return &Reconciler{RuntimeClient: runtimeClient}, &scope{machine: newTestMachine(), infraMachine: newTestUnstructured("GenericInfrastructureMachine", "infrastructure.cluster.x-k8s.io/v1beta2", "infra")}
369369
},
370-
wantErr: true,
371-
wantErrSubstrings: []string{
372-
"multiple extensions registered for UpdateMachine hook",
373-
"only one extension is supported",
374-
"ext-a",
375-
"ext-b",
376-
},
370+
wantErr: true,
371+
wantErrSubstrings: []string{"found multiple UpdateMachine hooks (ext-a,ext-b): only one hook is supported"},
377372
},
378373
{
379374
name: "fails when hook invocation returns error",

0 commit comments

Comments
 (0)