Skip to content

Add support for security groups #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions api/v1alpha1/scalewaymachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"`
}

Expand Down
30 changes: 30 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
60 changes: 53 additions & 7 deletions docs/scalewaymachine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
```
32 changes: 31 additions & 1 deletion internal/service/scaleway/client/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
}
59 changes: 46 additions & 13 deletions internal/service/scaleway/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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
Expand Down