Skip to content

Commit b2d2dca

Browse files
committed
Implement UpdateMachine driver interface
1 parent 5e1c267 commit b2d2dca

File tree

6 files changed

+413
-3
lines changed

6 files changed

+413
-3
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ require (
2424
sigs.k8s.io/yaml v1.6.0
2525
)
2626

27+
replace github.com/gardener/machine-controller-manager => github.com/afritzler/machine-controller-manager v0.0.0-20250514112441-bbd98c0e8666
28+
2729
require (
2830
github.com/Masterminds/goutils v1.1.1 // indirect
2931
github.com/Masterminds/semver v1.5.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
66
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
77
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
88
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
9+
github.com/afritzler/machine-controller-manager v0.0.0-20250514112441-bbd98c0e8666 h1:S/O5LahLcH28kJuAz/lkkGkWvHF0cZLxbCoDtSda1To=
10+
github.com/afritzler/machine-controller-manager v0.0.0-20250514112441-bbd98c0e8666/go.mod h1:TCU/KoudCMt2eV0Jnrq2D1TwgsrBCuhIVgV3j1el6Og=
911
github.com/aws/aws-sdk-go-v2 v1.38.2 h1:QUkLO1aTW0yqW95pVzZS0LGFanL71hJ0a49w4TJLMyM=
1012
github.com/aws/aws-sdk-go-v2 v1.38.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
1113
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -43,8 +45,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
4345
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
4446
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
4547
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
46-
github.com/gardener/machine-controller-manager v0.60.0 h1:aaSE85Yu0hcHYsP5/x1rxWa5o2zhmsmXlKQ+xefHY/Q=
47-
github.com/gardener/machine-controller-manager v0.60.0/go.mod h1:8eE1qLztrWIbOM71mHSQGaC6Q+pl5lvOyN08qP39D7o=
4848
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
4949
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
5050
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=

pkg/metal/create_machine.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import (
1616
"github.com/gardener/machine-controller-manager/pkg/util/provider/driver"
1717
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes"
1818
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status"
19-
19+
apiv1alpha1 "github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/api/v1alpha1"
20+
"github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/api/validation"
21+
"github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/ignition"
22+
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
2023
corev1 "k8s.io/api/core/v1"
2124
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
26+
"k8s.io/apimachinery/pkg/util/validation/field"
2227
"k8s.io/klog/v2"
2328
"sigs.k8s.io/controller-runtime/pkg/client"
2429
)

pkg/metal/helper.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package metal
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"time"
11+
12+
"github.com/gardener/machine-controller-manager/pkg/apis/machine/v1alpha1"
13+
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes"
14+
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status"
15+
"github.com/imdario/mergo"
16+
apiv1alpha1 "github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/api/v1alpha1"
17+
"github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/ignition"
18+
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
19+
corev1 "k8s.io/api/core/v1"
20+
apierrors "k8s.io/apimachinery/pkg/api/errors"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
23+
"k8s.io/apimachinery/pkg/util/wait"
24+
"k8s.io/klog/v2"
25+
capiv1beta1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
)
28+
29+
// applyServerClaim reserves a Server by creating a corresponding ServerClaim object with proper ignition data
30+
func (d *metalDriver) applyServerClaim(ctx context.Context, machine *v1alpha1.Machine, providerSpec *apiv1alpha1.ProviderSpec, ignitionSecret *corev1.Secret) (*metalv1alpha1.ServerClaim, error) {
31+
serverClaim := &metalv1alpha1.ServerClaim{
32+
TypeMeta: metav1.TypeMeta{
33+
APIVersion: metalv1alpha1.GroupVersion.String(),
34+
Kind: "ServerClaim",
35+
},
36+
ObjectMeta: metav1.ObjectMeta{
37+
Name: machine.Name,
38+
Namespace: d.metalNamespace,
39+
Labels: providerSpec.Labels,
40+
},
41+
Spec: metalv1alpha1.ServerClaimSpec{
42+
Power: "On",
43+
ServerSelector: &metav1.LabelSelector{
44+
MatchLabels: providerSpec.ServerLabels,
45+
MatchExpressions: nil,
46+
},
47+
IgnitionSecretRef: &corev1.LocalObjectReference{Name: ignitionSecret.Name},
48+
Image: providerSpec.Image,
49+
},
50+
}
51+
52+
d.clientProvider.Lock()
53+
defer d.clientProvider.Unlock()
54+
metalClient := d.clientProvider.Client
55+
56+
if err := metalClient.Patch(ctx, serverClaim, client.Apply, fieldOwner, client.ForceOwnership); err != nil {
57+
return nil, status.Error(codes.Internal, fmt.Sprintf("error applying metal machine: %s", err.Error()))
58+
}
59+
60+
if err := metalClient.Patch(ctx, ignitionSecret, client.Apply, fieldOwner, client.ForceOwnership); err != nil {
61+
return nil, status.Error(codes.Internal, fmt.Sprintf("error applying ignition secret: %s", err.Error()))
62+
}
63+
64+
return serverClaim, nil
65+
}
66+
67+
// generateIgnitionSecret creates an ignition file for the machine and stores it in a secret
68+
func (d *metalDriver) generateIgnitionSecret(ctx context.Context, machine *v1alpha1.Machine, machineClassSecret *corev1.Secret, providerSpec *apiv1alpha1.ProviderSpec, addressesMetaData map[string]any) (*corev1.Secret, error) {
69+
// Get userData from machine secret
70+
userData, ok := machineClassSecret.Data["userData"]
71+
if !ok {
72+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to find user-data in machine secret %s", client.ObjectKeyFromObject(machineClassSecret)))
73+
}
74+
75+
// Ensure providerSpec.MetaData is a map[string]any
76+
if providerSpec.Metadata == nil {
77+
providerSpec.Metadata = make(map[string]any)
78+
}
79+
80+
// Merge addressesMetaData into providerSpec.MetaData
81+
if err := mergo.Merge(&providerSpec.Metadata, addressesMetaData, mergo.WithOverride); err != nil {
82+
return nil, fmt.Errorf("failed to merge addressesMetaData into providerSpec.MetaData: %w", err)
83+
}
84+
85+
// Construct ignition file config
86+
config := &ignition.Config{
87+
Hostname: machine.Name,
88+
UserData: string(userData),
89+
MetaData: providerSpec.Metadata,
90+
Ignition: providerSpec.Ignition,
91+
DnsServers: providerSpec.DnsServers,
92+
IgnitionOverride: providerSpec.IgnitionOverride,
93+
}
94+
ignitionContent, err := ignition.File(config)
95+
if err != nil {
96+
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create ignition file for machine %s: %v", machine.Name, err))
97+
}
98+
99+
ignitionData := map[string][]byte{}
100+
ignitionData["ignition"] = []byte(ignitionContent)
101+
ignitionSecret := &corev1.Secret{
102+
TypeMeta: metav1.TypeMeta{
103+
APIVersion: corev1.SchemeGroupVersion.String(),
104+
Kind: "Secret",
105+
},
106+
ObjectMeta: metav1.ObjectMeta{
107+
Name: d.getIgnitionNameForMachine(ctx, machine.Name),
108+
Namespace: d.metalNamespace,
109+
},
110+
Data: ignitionData,
111+
}
112+
113+
return ignitionSecret, nil
114+
}
115+
116+
// getOrCreateIPAddressClaims gets or creates IPAddressClaims for the ipam config
117+
func (d *metalDriver) getOrCreateIPAddressClaims(ctx context.Context, machine *v1alpha1.Machine, providerSpec *apiv1alpha1.ProviderSpec) ([]*capiv1beta1.IPAddressClaim, map[string]any, error) {
118+
var ipAddressClaims []*capiv1beta1.IPAddressClaim
119+
addressesMetaData := make(map[string]any)
120+
121+
d.clientProvider.Lock()
122+
defer d.clientProvider.Unlock()
123+
metalClient := d.clientProvider.Client
124+
125+
for _, networkRef := range providerSpec.IPAMConfig {
126+
ipAddrClaimName := fmt.Sprintf("%s-%s", machine.Name, networkRef.MetadataKey)
127+
if len(ipAddrClaimName) > utilvalidation.DNS1123SubdomainMaxLength {
128+
klog.Info("IP address claim name is too long, it will be shortened which can cause name collisions", "name", ipAddrClaimName)
129+
ipAddrClaimName = ipAddrClaimName[:utilvalidation.DNS1123SubdomainMaxLength]
130+
}
131+
132+
ipAddrClaimKey := client.ObjectKey{Namespace: d.metalNamespace, Name: ipAddrClaimName}
133+
ipClaim := &capiv1beta1.IPAddressClaim{}
134+
if err := metalClient.Get(ctx, ipAddrClaimKey, ipClaim); err != nil && !apierrors.IsNotFound(err) {
135+
return nil, nil, err
136+
} else if err == nil {
137+
klog.V(3).Infof("IP address claim found %s", ipAddrClaimKey.String())
138+
if ipClaim.Status.AddressRef.Name == "" {
139+
return nil, nil, fmt.Errorf("IP address claim %q has no IP address reference", ipAddrClaimKey.String())
140+
}
141+
if ipClaim.Labels == nil {
142+
return nil, nil, fmt.Errorf("IP address claim %q has no server claim labels", ipAddrClaimKey.String())
143+
}
144+
name, nameExists := ipClaim.Labels[LabelKeyServerClaimName]
145+
namespace, namespaceExists := ipClaim.Labels[LabelKeyServerClaimNamespace]
146+
if !nameExists || !namespaceExists {
147+
return nil, nil, fmt.Errorf("IP address claim %q has no server claim labels", ipAddrClaimKey.String())
148+
}
149+
if name != machine.Name || namespace != d.metalNamespace {
150+
return nil, nil, fmt.Errorf("IP address claim %q's server claim labels don't match. Expected: name: %q, namespace: %q. Actual: name: %q, namespace: %q", ipAddrClaimKey.String(), machine.Name, d.metalNamespace, name, namespace)
151+
}
152+
} else if apierrors.IsNotFound(err) {
153+
if networkRef.IPAMRef == nil {
154+
return nil, nil, errors.New("ipamRef of an ipamConfig is not set")
155+
}
156+
klog.V(3).Info("creating IP address claim", "name", ipAddrClaimKey.String())
157+
apiGroup := networkRef.IPAMRef.APIGroup
158+
ipClaim = &capiv1beta1.IPAddressClaim{
159+
ObjectMeta: metav1.ObjectMeta{
160+
Name: ipAddrClaimKey.Name,
161+
Namespace: ipAddrClaimKey.Namespace,
162+
Labels: map[string]string{
163+
LabelKeyServerClaimName: machine.Name,
164+
LabelKeyServerClaimNamespace: d.metalNamespace,
165+
},
166+
},
167+
Spec: capiv1beta1.IPAddressClaimSpec{
168+
PoolRef: corev1.TypedLocalObjectReference{
169+
APIGroup: &apiGroup,
170+
Kind: networkRef.IPAMRef.Kind,
171+
Name: networkRef.IPAMRef.Name,
172+
},
173+
},
174+
}
175+
if err = metalClient.Create(ctx, ipClaim); err != nil {
176+
return nil, nil, fmt.Errorf("error creating IP: %w", err)
177+
}
178+
179+
// Wait for the IP address claim to reach the ready state
180+
err = wait.PollUntilContextTimeout(
181+
ctx,
182+
time.Millisecond*50,
183+
time.Millisecond*340,
184+
true,
185+
func(ctx context.Context) (bool, error) {
186+
if err = metalClient.Get(ctx, ipAddrClaimKey, ipClaim); err != nil && !apierrors.IsNotFound(err) {
187+
return false, err
188+
}
189+
return ipClaim.Status.AddressRef.Name != "", nil
190+
})
191+
if err != nil {
192+
return nil, nil, err
193+
}
194+
}
195+
196+
ipAddrKey := client.ObjectKey{Namespace: ipClaim.Namespace, Name: ipClaim.Status.AddressRef.Name}
197+
ipAddr := &capiv1beta1.IPAddress{}
198+
if err := metalClient.Get(ctx, ipAddrKey, ipAddr); err != nil {
199+
return nil, nil, err
200+
}
201+
202+
ipAddressClaims = append(ipAddressClaims, ipClaim)
203+
addressesMetaData[networkRef.MetadataKey] = map[string]any{
204+
"ip": ipAddr.Spec.Address,
205+
"prefix": ipAddr.Spec.Prefix,
206+
"gateway": ipAddr.Spec.Gateway,
207+
}
208+
}
209+
return ipAddressClaims, addressesMetaData, nil
210+
}

pkg/metal/update_machine.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package metal
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/gardener/machine-controller-manager/pkg/util/provider/driver"
11+
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes"
12+
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status"
13+
apiv1alpha1 "github.com/ironcore-dev/machine-controller-manager-provider-ironcore-metal/pkg/api/v1alpha1"
14+
"k8s.io/klog/v2"
15+
)
16+
17+
func (d *metalDriver) UpdateMachine(ctx context.Context, req *driver.UpdateMachineRequest) (*driver.UpdateMachineResponse, error) {
18+
if isEmptyUpdateRequest(req) {
19+
return nil, status.Error(codes.InvalidArgument, "received empty request")
20+
}
21+
22+
if req.MachineClass.Provider != apiv1alpha1.ProviderName {
23+
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("requested provider '%s' is not supported by the driver '%s'", req.MachineClass.Provider, apiv1alpha1.ProviderName))
24+
}
25+
26+
klog.V(3).Infof("Machine update request has been received for %q", req.Machine.Name)
27+
defer klog.V(3).Infof("Machine update request has been processed for %q", req.Machine.Name)
28+
29+
providerSpec, err := validateProviderSpecAndSecret(req.MachineClass, req.Secret)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
addressClaims, addressesMetaData, err := d.getOrCreateIPAddressClaims(ctx, req.Machine, providerSpec)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
ignitionSecret, err := d.generateIgnitionSecret(ctx, req.Machine, req.Secret, providerSpec, addressesMetaData)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
serverClaim, err := d.applyServerClaim(ctx, req.Machine, providerSpec, ignitionSecret)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
if err := d.setServerClaimOwnership(ctx, serverClaim, addressClaims); err != nil {
50+
return nil, err
51+
}
52+
53+
return &driver.UpdateMachineResponse{}, nil
54+
}
55+
56+
func isEmptyUpdateRequest(req *driver.UpdateMachineRequest) bool {
57+
return req == nil || req.MachineClass == nil || req.Machine == nil || req.Secret == nil
58+
}

0 commit comments

Comments
 (0)