Skip to content
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
16 changes: 6 additions & 10 deletions docs/stackit-iaas-api-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,21 +213,17 @@ Example: `stackit://my-project-123/550e8400-e29b-41d4-a716-446655440000`

Use the `labels` field for MCM identification and mapping:

| Label Key | Value | Purpose |
| --------------------------------- | ----------------- | ----------------------------------------------- |
| `mcm.gardener.cloud/cluster` | Cluster ID | Identify which cluster owns this server |
| `mcm.gardener.cloud/machine` | Machine CR name | Map server to Kubernetes Machine |
| `mcm.gardener.cloud/machineclass` | MachineClass name | Map server to MachineClass for orphan detection |
| `mcm.gardener.cloud/role` | "node" | Identify as cluster node |
| Label Key | Value | Purpose |
| ---------------------------- | ----------------- | ----------------------------------------------- |
| `kubernetes.io/machine` | Machine CR name | Map server to Kubernetes Machine |
| `kubernetes.io/machineclass` | MachineClass name | Map server to MachineClass for orphan detection |

Example labels:

```json
{
"mcm.gardener.cloud/cluster": "shoot-dev-01",
"mcm.gardener.cloud/machine": "worker-pool-a-12345",
"mcm.gardener.cloud/machineclass": "worker-pool-a",
"mcm.gardener.cloud/role": "node"
"kubernetes.io/machine": "worker-pool-a-12345",
"kubernetes.io/machineclass": "worker-pool-a"
}
```

Expand Down
6 changes: 2 additions & 4 deletions pkg/client/sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,14 @@ var _ = Describe("SDK Type Conversion Helpers", func() {

It("should convert labels with special characters", func() {
labels := map[string]string{
"mcm.gardener.cloud/machine": "test-machine",
"kubernetes.io/role": "node",
"kubernetes.io/machine": "test-machine",
}

result := convertLabelsToSDK(labels)

Expect(result).NotTo(BeNil())
Expect(result).To(HaveLen(2))
Expect(result["mcm.gardener.cloud/machine"]).To(Equal("test-machine"))
Expect(result["kubernetes.io/role"]).To(Equal("node"))
Expect(result["kubernetes.io/machine"]).To(Equal("test-machine"))
})
})

Expand Down
6 changes: 3 additions & 3 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ var regionRegex = regexp.MustCompile(`^[a-z0-9]+$`)
// Pattern: lowercase letters/digits followed by digits, dash, then digit(s) (e.g., eu01-1, eu01-2)
var availabilityZoneRegex = regexp.MustCompile(`^[a-z0-9]+-\d+$`)

// labelKeyRegex validates Kubernetes label keys (must start/end with alphanumeric, can contain -, _, .)
// labelKeyRegex validates Kubernetes label keys (must start/end with alphanumeric, can contain -, _, ., /)
// Maximum length: 63 characters
var labelKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9]([-a-zA-Z0-9_.]*[a-zA-Z0-9])?$`)
var labelKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9]([-a-zA-Z0-9_./]*[a-zA-Z0-9])?$`)

// labelValueRegex validates Kubernetes label values (must start/end with alphanumeric, can contain -, _, ., can be empty)
// Maximum length: 63 characters
Expand Down Expand Up @@ -113,7 +113,7 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
errors = append(errors, fmt.Errorf("providerSpec.labels key '%s' exceeds maximum length of 63 characters", key))
}
if !labelKeyRegex.MatchString(key) {
errors = append(errors, fmt.Errorf("providerSpec.labels key '%s' has invalid format (must start/end with alphanumeric, can contain -, _, .)", key))
errors = append(errors, fmt.Errorf("providerSpec.labels key '%s' has invalid format (must start/end with alphanumeric, can contain -, _, ., /)", key))
}
if len(value) > 63 {
errors = append(errors, fmt.Errorf("providerSpec.labels value for key '%s' exceeds maximum length of 63 characters", key))
Expand Down
11 changes: 11 additions & 0 deletions pkg/provider/apis/validation/validation_core_labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
"app.kubernetes.io_component": "worker",
"environment-type": "prod",
"version": "v1.2.3",
"app/component": "core",
}
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors).To(BeEmpty())
Expand Down Expand Up @@ -168,6 +169,16 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
Expect(errors[0].Error()).To(ContainSubstring("invalid format"))
})

It("should succeed with label keys containing slashes", func() {
providerSpec.Labels = map[string]string{
"mycompany.com/environment": "prod",
"app.io/version": "v2",
"team/project/name": "web",
}
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors).To(BeEmpty())
})

It("should fail when label key contains invalid characters", func() {
providerSpec.Labels = map[string]string{
"invalid@key": "value",
Expand Down
5 changes: 2 additions & 3 deletions pkg/provider/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import (

const (
StackitProviderName = "stackit"
StackitMachineLabel = "mcm.gardener.cloud/machine"
StackitMachineClassLabel = "mcm.gardener.cloud/machineclass"
StackitRoleLabel = "mcm.gardener.cloud/role"
StackitMachineLabel = "kubernetes.io/machine"
StackitMachineClassLabel = "kubernetes.io/machineclass"
)

// GetVolumeIDs extracts volume IDs from PersistentVolume specs
Expand Down
8 changes: 4 additions & 4 deletions pkg/provider/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"maps"
"slices"

"github.com/gardener/machine-controller-manager/pkg/util/provider/driver"
Expand Down Expand Up @@ -94,16 +95,15 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
func (p *Provider) createServerRequest(req *driver.CreateMachineRequest, providerSpec *api.ProviderSpec) *client.CreateServerRequest {
// Build labels: merge ProviderSpec labels with MCM-specific labels
labels := make(map[string]string)

// Start with user-provided labels from ProviderSpec
if providerSpec.Labels != nil {
for k, v := range providerSpec.Labels {
labels[k] = v
}
maps.Copy(labels, providerSpec.Labels)
}

// Add MCM-specific labels for server identification and orphan VM detection
labels[StackitMachineLabel] = req.Machine.Name
labels[StackitMachineClassLabel] = req.MachineClass.Name
labels[StackitRoleLabel] = "node"

// Create server request
createReq := &client.CreateServerRequest{
Expand Down
4 changes: 2 additions & 2 deletions pkg/provider/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// ListMachines lists all STACKIT servers that belong to the specified MachineClass
//
// This method retrieves all servers in the STACKIT project and filters them based on
// the "mcm.gardener.cloud/machineclass" label. This enables the MCM safety controller
// the "kubernetes.io/machineclass" label. This enables the MCM safety controller
// to detect and clean up orphan VMs that are not backed by Machine CRs.
//
// Returns:
Expand Down Expand Up @@ -51,7 +51,7 @@ func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesReq
}

// Filter servers by MachineClass label
// We use the "mcm.gardener.cloud/machineclass" label to identify which servers belong to this MachineClass
// We use the "kubernetes.io/machineclass" label to identify which servers belong to this MachineClass
machineList := make(map[string]string)
for _, server := range servers {
// Generate ProviderID in format: stackit://<projectId>/<serverId>
Expand Down
10 changes: 5 additions & 5 deletions pkg/provider/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,23 @@ var _ = Describe("ListMachines", func() {
Context("with valid inputs", func() {
It("should list machines filtered by MachineClass label", func() {
mockClient.ListServersFunc = func(_ context.Context, _, _ string, selector map[string]string) ([]*client.Server, error) {
Expect(selector["mcm.gardener.cloud/machineclass"]).To(Equal("test-machine-class"))
Expect(selector["kubernetes.io/machineclass"]).To(Equal("test-machine-class"))

return []*client.Server{
{
ID: "server-1",
Name: "machine-1",
Labels: map[string]string{
"mcm.gardener.cloud/machineclass": "test-machine-class",
"mcm.gardener.cloud/machine": "machine-1",
"kubernetes.io/machineclass": "test-machine-class",
"kubernetes.io/machine": "machine-1",
},
},
{
ID: "server-2",
Name: "machine-2",
Labels: map[string]string{
"mcm.gardener.cloud/machineclass": "test-machine-class",
"mcm.gardener.cloud/machine": "machine-2",
"kubernetes.io/machineclass": "test-machine-class",
"kubernetes.io/machine": "machine-2",
},
},
}, nil
Expand Down
5 changes: 2 additions & 3 deletions test/e2e/e2e_labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,8 @@ spec:

// Verify that MCM-generated labels would still be sent (check provider behavior)
// Even without user labels, the provider should add MCM labels like:
// - mcm.gardener.cloud/machineclass
// - mcm.gardener.cloud/machine
// - mcm.gardener.cloud/role
// - kubernetes.io/machineclass
// - kubernetes.io/machine
By("verifying provider handles missing labels gracefully")

// Check provider logs for any errors related to missing labels
Expand Down