From 61d975f037c6f8a36a7dd522fc2dcd0cd5cfcd6b Mon Sep 17 00:00:00 2001 From: Tomy Guichard Date: Thu, 19 Jun 2025 16:20:13 +0200 Subject: [PATCH] Add support for security groups --- api/v1alpha1/scalewaymachine_types.go | 20 +++++++ api/v1alpha1/zz_generated.deepcopy.go | 30 ++++++++++ ...ure.cluster.x-k8s.io_scalewaymachines.yaml | 18 ++++++ ...ter.x-k8s.io_scalewaymachinetemplates.yaml | 19 ++++++ docs/scalewaymachine.md | 60 ++++++++++++++++--- internal/service/scaleway/client/instance.go | 32 +++++++++- .../service/scaleway/instance/instance.go | 59 ++++++++++++++---- 7 files changed, 217 insertions(+), 21 deletions(-) diff --git a/api/v1alpha1/scalewaymachine_types.go b/api/v1alpha1/scalewaymachine_types.go index 5d8daac..a91afe4 100644 --- a/api/v1alpha1/scalewaymachine_types.go +++ b/api/v1alpha1/scalewaymachine_types.go @@ -11,6 +11,7 @@ const MachineFinalizer = "scalewaycluster.infrastructure.cluster.x-k8s.io/sm-pro // +kubebuilder:validation:XValidation:rule="has(self.rootVolume) == has(oldSelf.rootVolume)",message="rootVolume cannot be added or removed" // +kubebuilder:validation:XValidation:rule="has(self.publicNetwork) == has(oldSelf.publicNetwork)",message="publicNetwork cannot be added or removed" // +kubebuilder:validation:XValidation:rule="has(self.placementGroup) == has(oldSelf.placementGroup)",message="placementGroup cannot be added or removed" +// +kubebuilder:validation:XValidation:rule="has(self.securityGroup) == has(oldSelf.securityGroup)",message="securityGroup cannot be added or removed" type ScalewayMachineSpec struct { // ProviderID must match the provider ID as seen on the node object corresponding to this machine. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" @@ -40,6 +41,11 @@ type ScalewayMachineSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" // +optional PlacementGroup *PlacementGroupSpec `json:"placementGroup,omitempty"` + + // SecurityGroup allows attaching a Security Group to the instance. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +optional + SecurityGroup *SecurityGroupSpec `json:"securityGroup,omitempty"` } // RootVolumeSpec defines the characteristics of the system (root) volume. @@ -72,11 +78,25 @@ type PublicNetworkSpec struct { EnableIPv6 *bool `json:"enableIPv6,omitempty"` } +// PlacementGroupSpec contains an ID or Name of an existing Placement Group. // +kubebuilder:validation:XValidation:rule="(has(self.id) ? 1 : 0) + (has(self.name) ? 1 : 0) == 1",message="exactly one of id or name must be set" type PlacementGroupSpec struct { // ID of the placement group. + // +optional ID *string `json:"id,omitempty"` // Name of the placement group. + // +optional + Name *string `json:"name,omitempty"` +} + +// SecurityGroupSpec contains an ID or Name of an existing Security Group. +// +kubebuilder:validation:XValidation:rule="(has(self.id) ? 1 : 0) + (has(self.name) ? 1 : 0) == 1",message="exactly one of id or name must be set" +type SecurityGroupSpec struct { + // ID of the security group. + // +optional + ID *string `json:"id,omitempty"` + // +optional + // Name of the security group. Name *string `json:"name,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5336eaf..d11bf0d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -615,6 +615,11 @@ func (in *ScalewayMachineSpec) DeepCopyInto(out *ScalewayMachineSpec) { *out = new(PlacementGroupSpec) (*in).DeepCopyInto(*out) } + if in.SecurityGroup != nil { + in, out := &in.SecurityGroup, &out.SecurityGroup + *out = new(SecurityGroupSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalewayMachineSpec. @@ -737,3 +742,28 @@ func (in *ScalewayMachineTemplateSpec) DeepCopy() *ScalewayMachineTemplateSpec { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityGroupSpec) DeepCopyInto(out *SecurityGroupSpec) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityGroupSpec. +func (in *SecurityGroupSpec) DeepCopy() *SecurityGroupSpec { + if in == nil { + return nil + } + out := new(SecurityGroupSpec) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachines.yaml index 8c88f9b..7d0c669 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachines.yaml @@ -148,6 +148,22 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + securityGroup: + description: SecurityGroup allows attaching a Security Group to the + instance. + properties: + id: + description: ID of the security group. + type: string + name: + description: Name of the security group. + type: string + type: object + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + - message: exactly one of id or name must be set + rule: '(has(self.id) ? 1 : 0) + (has(self.name) ? 1 : 0) == 1' required: - commercialType - image @@ -159,6 +175,8 @@ spec: rule: has(self.publicNetwork) == has(oldSelf.publicNetwork) - message: placementGroup cannot be added or removed rule: has(self.placementGroup) == has(oldSelf.placementGroup) + - message: securityGroup cannot be added or removed + rule: has(self.securityGroup) == has(oldSelf.securityGroup) status: description: ScalewayMachineStatus defines the observed state of ScalewayMachine. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachinetemplates.yaml index 49c6185..d1ba4e4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_scalewaymachinetemplates.yaml @@ -152,6 +152,23 @@ spec: x-kubernetes-validations: - message: Value is immutable rule: self == oldSelf + securityGroup: + description: SecurityGroup allows attaching a Security Group + to the instance. + properties: + id: + description: ID of the security group. + type: string + name: + description: Name of the security group. + type: string + type: object + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + - message: exactly one of id or name must be set + rule: '(has(self.id) ? 1 : 0) + (has(self.name) ? 1 : 0) + == 1' required: - commercialType - image @@ -163,6 +180,8 @@ spec: rule: has(self.publicNetwork) == has(oldSelf.publicNetwork) - message: placementGroup cannot be added or removed rule: has(self.placementGroup) == has(oldSelf.placementGroup) + - message: securityGroup cannot be added or removed + rule: has(self.securityGroup) == has(oldSelf.securityGroup) required: - spec type: object diff --git a/docs/scalewaymachine.md b/docs/scalewaymachine.md index ffd77bd..6728dba 100644 --- a/docs/scalewaymachine.md +++ b/docs/scalewaymachine.md @@ -172,19 +172,19 @@ Private Network (`network.privateNetwork.enabled`) in the `ScalewayCluster`. ## Placement Group -It is possible to attach an existing Placement group to the Instance server that will be created. +It is possible to attach an existing placement group to the Instance server that will be created. > [!WARNING] -> A Placement group can be attached to at most 20 Instance servers. +> A placement group can be attached to at most 20 Instance servers. Placement groups allow you to define if you want certain Instances to run on different physical hypervisors for maximum availability or as physically close -together as possible for minimum latency. For more information about Placement +together as possible for minimum latency. For more information about placement groups, please refer to the [Scaleway documentation](https://www.scaleway.com/en/docs/instances/how-to/use-placement-groups/). The `placementGroup` field must contain one of the following: -- A Placement group ID: +- A placement group ID: ```yaml apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 @@ -198,7 +198,7 @@ The `placementGroup` field must contain one of the following: # some fields were omitted... ``` -- A Placement group name: +- A placement group name: ```yaml apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 @@ -212,9 +212,55 @@ The `placementGroup` field must contain one of the following: # some fields were omitted... ``` - Make sure this Placement group exists in the zones where you plan to deploy your nodes. - You can list Placement groups by name with this command: + Make sure this placement group exists in the zones where you plan to deploy your nodes. + You can list placement groups by name with this command: ```bash scw instance placement-group list name=${IMAGE_NAME} zone=${SCW_ZONE} ``` + +## Security Group + +It is possible to attach an existing security group to the Instance server that will be created. + +Security groups act as firewalls, filtering public internet traffic on your Instances. +They can be stateful or stateless, and allow you to create rules to drop or allow traffic +to and from your Instance. For more information about security groups, please refer +to the [Scaleway documentation](https://www.scaleway.com/en/docs/instances/how-to/use-security-groups/). + +The `securityGroup` field must contain one of the following: + +- A security group ID: + + ```yaml + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ScalewayMachine + metadata: + name: my-machine + namespace: default + spec: + securityGroup: + id: 11111111-1111-1111-1111-111111111111 + # some fields were omitted... + ``` + +- A security group name: + + ```yaml + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1 + kind: ScalewayMachine + metadata: + name: my-machine + namespace: default + spec: + securityGroup: + name: my-placement-group + # some fields were omitted... + ``` + + Make sure this security group exists in the zones where you plan to deploy your nodes. + You can list security groups by name with this command: + + ```bash + scw instance security-group list name=${IMAGE_NAME} zone=${SCW_ZONE} + ``` diff --git a/internal/service/scaleway/client/instance.go b/internal/service/scaleway/client/instance.go index 4f88d27..cbb20af 100644 --- a/internal/service/scaleway/client/instance.go +++ b/internal/service/scaleway/client/instance.go @@ -50,7 +50,7 @@ func (c *Client) CreateServer( ctx context.Context, zone scw.Zone, name, commercialType, imageID string, - placementGroupID *string, + placementGroupID, securityGroupID *string, rootVolumeSize scw.Size, rootVolumeType instance.VolumeVolumeType, publicIPs, tags []string, @@ -74,6 +74,7 @@ func (c *Client) CreateServer( DynamicIPRequired: scw.BoolPtr(false), Image: &imageID, PlacementGroup: placementGroupID, + SecurityGroup: securityGroupID, Volumes: map[string]*instance.VolumeServerTemplate{ "0": { Size: &rootVolumeSize, @@ -399,3 +400,32 @@ func (c *Client) FindPlacementGroup(ctx context.Context, zone scw.Zone, name str return nil, fmt.Errorf("%w: found %d placement groups with name %s", ErrTooManyItemsFound, len(placementGroups), name) } } + +func (c *Client) FindSecurityGroup(ctx context.Context, zone scw.Zone, name string) (*instance.SecurityGroup, error) { + if err := c.validateZone(c.instance, zone); err != nil { + return nil, err + } + + resp, err := c.instance.ListSecurityGroups(&instance.ListSecurityGroupsRequest{ + Zone: zone, + Name: &name, + Project: &c.projectID, + }, scw.WithContext(ctx), scw.WithAllPages()) + if err != nil { + return nil, newCallError("ListSecurityGroups", err) + } + + // Filter out all security groups that have the wrong name. + securityGroups := slices.DeleteFunc(resp.SecurityGroups, func(sg *instance.SecurityGroup) bool { + return sg.Name != name + }) + + switch len(securityGroups) { + case 0: + return nil, ErrNoItemFound + case 1: + return securityGroups[0], nil + default: + return nil, fmt.Errorf("%w: found %d security groups with name %s", ErrTooManyItemsFound, len(securityGroups), name) + } +} diff --git a/internal/service/scaleway/instance/instance.go b/internal/service/scaleway/instance/instance.go index a5c8621..3fbc91b 100644 --- a/internal/service/scaleway/instance/instance.go +++ b/internal/service/scaleway/instance/instance.go @@ -256,20 +256,14 @@ func (s *Service) ensureServer(ctx context.Context) (*instance.Server, error) { } } - // If user has specified a placement group, get its ID. - var placementGroupID *string - if s.ScalewayMachine.Spec.PlacementGroup != nil { - switch pgref := s.ScalewayMachine.Spec.PlacementGroup; { - case pgref.ID != nil: - placementGroupID = pgref.ID - case pgref.Name != nil: - placementGroup, err := s.ScalewayClient.FindPlacementGroup(ctx, zone, *pgref.Name) - if err != nil { - return nil, fmt.Errorf("failed to find placement group: %w", err) - } + placementGroupID, err := s.placementGroupID(ctx, zone) + if err != nil { + return nil, err + } - placementGroupID = &placementGroup.ID - } + securityGroupID, err := s.securityGroupID(ctx, zone) + if err != nil { + return nil, err } // Finally, create the server. @@ -280,6 +274,7 @@ func (s *Service) ensureServer(ctx context.Context) (*instance.Server, error) { s.ScalewayMachine.Spec.CommercialType, imageID, placementGroupID, + securityGroupID, s.RootVolumeSize(), volumeType, publicIPs, @@ -304,6 +299,44 @@ func (s *Service) ensureServer(ctx context.Context) (*instance.Server, error) { return server, nil } +func (s *Service) placementGroupID(ctx context.Context, zone scw.Zone) (*string, error) { + // If user has specified a placement group, get its ID. + if s.ScalewayMachine.Spec.PlacementGroup != nil { + switch pgref := s.ScalewayMachine.Spec.PlacementGroup; { + case pgref.ID != nil: + return pgref.ID, nil + case pgref.Name != nil: + placementGroup, err := s.ScalewayClient.FindPlacementGroup(ctx, zone, *pgref.Name) + if err != nil { + return nil, fmt.Errorf("failed to find placement group: %w", err) + } + + return &placementGroup.ID, nil + } + } + + return nil, nil +} + +func (s *Service) securityGroupID(ctx context.Context, zone scw.Zone) (*string, error) { + // If user has specified a security group, get its ID. + if s.ScalewayMachine.Spec.SecurityGroup != nil { + switch sgref := s.ScalewayMachine.Spec.SecurityGroup; { + case sgref.ID != nil: + return sgref.ID, nil + case sgref.Name != nil: + securityGroup, err := s.ScalewayClient.FindSecurityGroup(ctx, zone, *sgref.Name) + if err != nil { + return nil, fmt.Errorf("failed to find security group: %w", err) + } + + return &securityGroup.ID, nil + } + } + + return nil, nil +} + func (s *Service) ensurePrivateNIC(ctx context.Context, server *instance.Server) ([]*ipam.IP, error) { if !s.HasPrivateNetwork() { return nil, nil