Skip to content

Commit 0f25c9f

Browse files
authored
Merge pull request #3056 from jfpucheu/feat/allowedaddresspairs-mutable
✨ Make allowedAddressPairs on OpenStackMachine ports mutable
2 parents 34c6ce5 + ad24391 commit 0f25c9f

File tree

9 files changed

+777
-11
lines changed

9 files changed

+777
-11
lines changed

config/rbac/role.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.

controllers/openstackmachine_controller.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,9 +671,17 @@ func openStackMachineSpecToOpenStackServerSpec(openStackMachineSpec *infrav1.Ope
671671

672672
// If not ports are provided we create one.
673673
// Ports must have a network so if none is provided we use the default network.
674-
serverPorts := openStackMachineSpec.Ports
674+
// Deep-copy the ports so in-place mutations below (Network injection, SecurityGroups
675+
// merge) do not modify the original OpenStackMachineSpec referenced by the patchHelper
676+
// baseline, which would cause a spurious spec-immutability webhook rejection.
677+
var serverPorts []infrav1.PortOpts
675678
if len(openStackMachineSpec.Ports) == 0 {
676679
serverPorts = make([]infrav1.PortOpts, 1)
680+
} else {
681+
serverPorts = make([]infrav1.PortOpts, len(openStackMachineSpec.Ports))
682+
for i, p := range openStackMachineSpec.Ports {
683+
serverPorts[i] = *p.DeepCopy()
684+
}
677685
}
678686

679687
var clusterSubnets []infrav1.FixedIP

controllers/openstackmachinetemplate_controller.go

Lines changed: 169 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package controllers
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223

@@ -26,6 +27,7 @@ import (
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728
kerrors "k8s.io/apimachinery/pkg/util/errors"
2829
"k8s.io/client-go/tools/events"
30+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
2931
"sigs.k8s.io/cluster-api/util"
3032
"sigs.k8s.io/cluster-api/util/annotations"
3133
conditions "sigs.k8s.io/cluster-api/util/conditions"
@@ -35,16 +37,28 @@ import (
3537
"sigs.k8s.io/controller-runtime/pkg/client"
3638
"sigs.k8s.io/controller-runtime/pkg/controller"
3739

40+
infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
3841
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta2"
3942
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/compute"
43+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking"
4044
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
4145
controllers "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/controllers"
4246
)
4347

44-
const imagePropertyForOS = "os_type"
48+
const (
49+
imagePropertyForOS = "os_type"
50+
51+
// annotationAllowedAddressPairs tracks the last-applied allowedAddressPairs per
52+
// OpenStackMachine (stored as JSON). Written as a metadata annotation so we never
53+
// touch the immutable OSM spec, which would trigger the spec-immutability webhook.
54+
annotationAllowedAddressPairs = "infrastructure.cluster.x-k8s.io/osmt-allowed-address-pairs"
55+
)
4556

4657
// Set here so we can easily mock it in tests.
47-
var newComputeService = compute.NewService
58+
var (
59+
newComputeService = compute.NewService
60+
newNetworkingService = networking.NewService
61+
)
4862

4963
// OpenStackMachineTemplateReconciler reconciles a OpenStackMachineTemplate object.
5064
// it only updates the .status field to allow auto-scaling.
@@ -58,6 +72,10 @@ type OpenStackMachineTemplateReconciler struct {
5872

5973
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplates,verbs=get;list;watch
6074
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplates/status,verbs=get;update;patch
75+
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachines,verbs=get;list;watch;patch
76+
// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackservers,verbs=get;list;watch
77+
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinesets,verbs=get;list;watch
78+
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines,verbs=get;list;watch
6179

6280
func (r *OpenStackMachineTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, reterr error) {
6381
log := ctrl.LoggerFrom(ctx)
@@ -142,14 +160,14 @@ func (r *OpenStackMachineTemplateReconciler) Reconcile(ctx context.Context, req
142160
scope := scope.NewWithLogger(clientScope, log)
143161

144162
// Handle non-deleted OpenStackMachineTemplates
145-
if err := r.reconcileNormal(ctx, scope, openStackMachineTemplate); err != nil {
163+
if err := r.reconcileNormal(ctx, scope, cluster.Name, openStackMachineTemplate); err != nil {
146164
return ctrl.Result{}, err
147165
}
148166
log.V(4).Info("Successfully reconciled OpenStackMachineTemplate")
149167
return ctrl.Result{}, nil
150168
}
151169

152-
func (r *OpenStackMachineTemplateReconciler) reconcileNormal(ctx context.Context, scope *scope.WithLogger, openStackMachineTemplate *infrav1.OpenStackMachineTemplate) (reterr error) {
170+
func (r *OpenStackMachineTemplateReconciler) reconcileNormal(ctx context.Context, scope *scope.WithLogger, clusterName string, openStackMachineTemplate *infrav1.OpenStackMachineTemplate) (reterr error) {
153171
log := scope.Logger()
154172

155173
computeService, err := newComputeService(scope)
@@ -201,10 +219,19 @@ func (r *OpenStackMachineTemplateReconciler) reconcileNormal(ctx context.Context
201219
openStackMachineTemplate.Status.Capacity[corev1.ResourceStorage] = *resource.NewQuantity(storageBytes, resource.BinarySI)
202220
}
203221

222+
// reconcileAllowedAddressPairs is called independently of the image/flavor logic so that
223+
// it is never skipped by an early return (e.g. when imageID is not yet resolvable).
224+
if err := r.reconcileAllowedAddressPairs(ctx, scope, clusterName, openStackMachineTemplate); err != nil {
225+
return err
226+
}
227+
204228
imageID, err := computeService.GetImageID(ctx, r.Client, openStackMachineTemplate.Namespace, openStackMachineTemplate.Spec.Template.Spec.Image)
205229
if err != nil {
206230
return err
207231
}
232+
if imageID == nil {
233+
return nil
234+
}
208235

209236
image, err := computeService.GetImageDetails(*imageID)
210237
if err != nil {
@@ -224,6 +251,144 @@ func (r *OpenStackMachineTemplateReconciler) reconcileNormal(ctx context.Context
224251
return nil
225252
}
226253

254+
// reconcileAllowedAddressPairs updates the allowedAddressPairs on existing Neutron ports
255+
// to match what is defined in the OpenStackMachineTemplate.
256+
// Idempotency is tracked via an annotation on each OpenStackMachine so that only a
257+
// metadata-only patch is needed — this avoids touching the immutable OSM spec.
258+
func (r *OpenStackMachineTemplateReconciler) reconcileAllowedAddressPairs(ctx context.Context, scope *scope.WithLogger, clusterName string, openStackMachineTemplate *infrav1.OpenStackMachineTemplate) error {
259+
log := scope.Logger()
260+
261+
if len(openStackMachineTemplate.Spec.Template.Spec.Ports) == 0 || clusterName == "" {
262+
return nil
263+
}
264+
265+
// Build the desired state as JSON for idempotency comparison.
266+
type portPairs = []infrav1.AddressPair
267+
templatePorts := openStackMachineTemplate.Spec.Template.Spec.Ports
268+
desired := make([]portPairs, len(templatePorts))
269+
for i, p := range templatePorts {
270+
desired[i] = p.AllowedAddressPairs
271+
}
272+
desiredJSON, err := json.Marshal(desired)
273+
if err != nil {
274+
return err
275+
}
276+
desiredStr := string(desiredJSON)
277+
278+
// List MachineSets in the namespace for this cluster.
279+
machineSetList := &clusterv1.MachineSetList{}
280+
if err := r.Client.List(ctx, machineSetList,
281+
client.InNamespace(openStackMachineTemplate.Namespace),
282+
client.MatchingLabels{clusterv1.ClusterNameLabel: clusterName},
283+
); err != nil {
284+
return err
285+
}
286+
287+
// List Machines in the namespace for this cluster.
288+
machineList := &clusterv1.MachineList{}
289+
if err := r.Client.List(ctx, machineList,
290+
client.InNamespace(openStackMachineTemplate.Namespace),
291+
client.MatchingLabels{clusterv1.ClusterNameLabel: clusterName},
292+
); err != nil {
293+
return err
294+
}
295+
296+
// Networking service is initialised lazily on first actual port update.
297+
var networkingService *networking.Service
298+
299+
for i := range machineSetList.Items {
300+
ms := &machineSetList.Items[i]
301+
if ms.Spec.Template.Spec.InfrastructureRef.Name != openStackMachineTemplate.Name {
302+
continue
303+
}
304+
305+
for j := range machineList.Items {
306+
machine := &machineList.Items[j]
307+
if !isOwnedByMachineSet(machine, ms) {
308+
continue
309+
}
310+
infraName := machine.Spec.InfrastructureRef.Name
311+
if infraName == "" {
312+
continue
313+
}
314+
315+
osm := &infrav1.OpenStackMachine{}
316+
if err := r.Client.Get(ctx, client.ObjectKey{
317+
Namespace: openStackMachineTemplate.Namespace,
318+
Name: infraName,
319+
}, osm); err != nil {
320+
if apierrors.IsNotFound(err) {
321+
continue
322+
}
323+
return err
324+
}
325+
326+
// Skip if annotation already reflects the desired state.
327+
if osm.Annotations[annotationAllowedAddressPairs] == desiredStr {
328+
continue
329+
}
330+
331+
// Port IDs are stored in the OpenStackServer status (same name as the OSM).
332+
openStackServer := &infrav1alpha1.OpenStackServer{}
333+
if err := r.Client.Get(ctx, client.ObjectKey{
334+
Namespace: openStackMachineTemplate.Namespace,
335+
Name: infraName,
336+
}, openStackServer); err != nil {
337+
if apierrors.IsNotFound(err) {
338+
continue
339+
}
340+
return err
341+
}
342+
343+
if openStackServer.Status.Resources == nil || len(openStackServer.Status.Resources.Ports) == 0 {
344+
continue
345+
}
346+
347+
if networkingService == nil {
348+
networkingService, err = newNetworkingService(scope)
349+
if err != nil {
350+
return err
351+
}
352+
}
353+
354+
for portIdx, portStatus := range openStackServer.Status.Resources.Ports {
355+
if portIdx >= len(templatePorts) {
356+
break
357+
}
358+
pairs := templatePorts[portIdx].AllowedAddressPairs
359+
log.Info("Updating allowedAddressPairs on port", "portID", portStatus.ID,
360+
"machine", osm.Name, "portIndex", portIdx)
361+
if err := networkingService.UpdateAllowedAddressPairs(portStatus.ID, pairs); err != nil {
362+
log.Error(err, "Failed to update allowedAddressPairs", "portID", portStatus.ID)
363+
return err
364+
}
365+
}
366+
367+
// Record the applied state in an annotation (metadata-only patch).
368+
osmCopy := osm.DeepCopy()
369+
if osm.Annotations == nil {
370+
osm.Annotations = map[string]string{}
371+
}
372+
osm.Annotations[annotationAllowedAddressPairs] = desiredStr
373+
if err := r.Client.Patch(ctx, osm, client.MergeFrom(osmCopy)); err != nil {
374+
return err
375+
}
376+
}
377+
}
378+
379+
return nil
380+
}
381+
382+
// isOwnedByMachineSet returns true if the Machine has an owner reference pointing to ms.
383+
func isOwnedByMachineSet(machine *clusterv1.Machine, ms *clusterv1.MachineSet) bool {
384+
for _, ref := range machine.OwnerReferences {
385+
if ref.Kind == "MachineSet" && ref.Name == ms.Name {
386+
return true
387+
}
388+
}
389+
return false
390+
}
391+
227392
func (r *OpenStackMachineTemplateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
228393
log := ctrl.LoggerFrom(ctx)
229394

0 commit comments

Comments
 (0)