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
2 changes: 1 addition & 1 deletion hack/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ else
timeout_flag="-timeout=2m"
fi

go test ${timeout_flag:+"$timeout_flag"} "$@" "${test_flags[@]}"
go test ${timeout_flag:+"$timeout_flag"} "$@" ${test_flags:+"${test_flags[@]}"}
4 changes: 4 additions & 0 deletions pkg/provider/apis/provider_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ package api

// ProviderSpec is the spec to be used while parsing the calls.
type ProviderSpec struct {
// Region is the STACKIT region (e.g., "eu01", "eu02")
// Required field for creating a server.
Region string `json:"region"`

// MachineType is the STACKIT server type (e.g., "c2i.2", "m2i.8")
// Required field for creating a server.
MachineType string `json:"machineType"`
Expand Down
26 changes: 18 additions & 8 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]
var machineTypeRegex = regexp.MustCompile(`^[a-z]+\d+[a-z]*\.\d+[a-z]*(\.[a-z]+\d+)*$`)

// regionRegex is a regex pattern for validating STACKIT region format
// Pattern: lowercase letters/digits (e.g., eu01, eu01)
var regionRegex = regexp.MustCompile(`^[a-z0-9]+$`)

// availabilityZoneRegex is a regex pattern for validating STACKIT availability zone format
// Pattern: lowercase letters/digits followed by digits, dash, then digit(s) (e.g., eu01-1, eu01-2)
var regionRegex = regexp.MustCompile(`^[a-z0-9]+-\d+$`)
var availabilityZoneRegex = regexp.MustCompile(`^[a-z0-9]+-\d+$`)

// labelKeyRegex validates Kubernetes label keys (must start/end with alphanumeric, can contain -, _, .)
// Maximum length: 63 characters
Expand Down Expand Up @@ -74,13 +78,14 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
}

// Validate region (required for SDK)
region, ok := secrets.Data["region"]
if !ok {
errors = append(errors, fmt.Errorf("secret field 'region' is required"))
} else if len(region) == 0 {
errors = append(errors, fmt.Errorf("secret field 'region' cannot be empty"))
} else if !isValidRegion(string(region)) {
errors = append(errors, fmt.Errorf("secret field 'region' has invalid format (expected format: eu01-1, eu01-2, etc.)"))
if spec.Region == "" {
errors = append(errors, fmt.Errorf("providerSpec.Region cannot be empty"))
} else if !isValidRegion(spec.Region) {
errors = append(errors, fmt.Errorf("providerSpec.Region has invalid format (expected format: eu01, eu02, etc.)"))
}

if spec.AvailabilityZone != "" && !isValidAvailabilityZone(spec.AvailabilityZone) {
errors = append(errors, fmt.Errorf("providerSpec.availabilityZone has invalid format (expected format: eu01-1, eu01-2, etc.)"))
}

// Validate ProviderSpec
Expand Down Expand Up @@ -287,6 +292,11 @@ func isValidRegion(s string) bool {
return regionRegex.MatchString(s)
}

// isValidAvailabilityZone checks if a string matches the STACKIT availability zone format
func isValidAvailabilityZone(s string) bool {
return availabilityZoneRegex.MatchString(s)
}

// isValidJSON checks if a string is valid JSON
func isValidJSON(s string) bool {
var js json.RawMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down
47 changes: 44 additions & 3 deletions pkg/provider/apis/validation/validation_fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down Expand Up @@ -70,6 +70,47 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
})
})

Context("Region validation", func() {
It("should succeed with valid region", func() {
providerSpec.Region = "eu01"
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors).To(BeEmpty())
})

It("should fail when region is empty", func() {
providerSpec.Region = ""
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors[0].Error()).To(ContainSubstring("Region cannot be empty"))
})

It("should succeed with various region formats", func() {
testCases := []string{
"eu01",
"eu02",
"eu10",
"us02",
}
for _, r := range testCases {
providerSpec.Region = r
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors).To(BeEmpty(), "Region %q should be valid", r)
}
})

It("should fail with various region formats", func() {
testCases := []string{
"eu01-1",
"eu02-b",
"us-east-01",
}
for _, r := range testCases {
providerSpec.Region = r
errors := ValidateProviderSpecNSecret(providerSpec, secret)
Expect(errors[0].Error()).To(ContainSubstring("Region has invalid format"))
}
})
})

Context("AvailabilityZone validation", func() {
It("should succeed with valid availabilityZone", func() {
providerSpec.AvailabilityZone = "eu01-1"
Expand All @@ -87,8 +128,8 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
testCases := []string{
"eu01-1",
"eu01-2",
"us-west-1a",
"zone-1",
"eu02-1",
"eu02-4",
}
for _, az := range testCases {
providerSpec.AvailabilityZone = az
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/apis/validation/validation_networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/apis/validation/validation_secgroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/apis/validation/validation_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/apis/validation/validation_volumes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ var _ = Describe("ValidateProviderSpecNSecret", func() {
providerSpec = &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "550e8400-e29b-41d4-a716-446655440000",
Region: "eu01",
}
secret = &corev1.Secret{
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
},
}
})
Expand Down
29 changes: 21 additions & 8 deletions pkg/provider/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
// Extract credentials from Secret
projectID := string(req.Secret.Data["project-id"])
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])
region := string(req.Secret.Data["region"])

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand Down Expand Up @@ -171,7 +170,7 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
}

// Call STACKIT API to create server
server, err := p.client.CreateServer(ctx, projectID, region, createReq)
server, err := p.client.CreateServer(ctx, projectID, providerSpec.Region, createReq)
if err != nil {
klog.Errorf("Failed to create server for machine %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create server: %v", err))
Expand Down Expand Up @@ -211,7 +210,6 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR

// Extract credentials from Secret
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])
region := string(req.Secret.Data["region"])

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand All @@ -227,8 +225,13 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid ProviderID format: %v", err))
}

providerSpec, err := decodeProviderSpec(req.MachineClass)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

// Call STACKIT API to delete server
err = p.client.DeleteServer(ctx, projectID, region, serverID)
err = p.client.DeleteServer(ctx, projectID, providerSpec.Region, serverID)
if err != nil {
// Check if server was not found (404) - this is OK for idempotency
if errors.Is(err, ErrServerNotFound) {
Expand Down Expand Up @@ -273,7 +276,6 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS

// Extract credentials from Secret
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])
region := string(req.Secret.Data["region"])

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand All @@ -290,8 +292,14 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid ProviderID format: %v", err))
}

// Decode ProviderSpec from MachineClass
providerSpec, err := decodeProviderSpec(req.MachineClass)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

// Call STACKIT API to get server status
server, err := p.client.GetServer(ctx, projectID, region, serverID)
server, err := p.client.GetServer(ctx, projectID, providerSpec.Region, serverID)
if err != nil {
// Check if server was not found (404)
if errors.Is(err, ErrServerNotFound) {
Expand Down Expand Up @@ -330,15 +338,20 @@ func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesReq
// Extract credentials from Secret
projectID := string(req.Secret.Data["project-id"])
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])
region := string(req.Secret.Data["region"])

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
}

// Decode ProviderSpec from MachineClass
providerSpec, err := decodeProviderSpec(req.MachineClass)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

// Call STACKIT API to list all servers
servers, err := p.client.ListServers(ctx, projectID, region)
servers, err := p.client.ListServers(ctx, projectID, providerSpec.Region)
if err != nil {
klog.Errorf("Failed to list servers for MachineClass %q: %v", req.MachineClass.Name, err)
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to list servers: %v", err))
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/core_create_machine_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ var _ = Describe("CreateMachine", func() {
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
"networkId": []byte("770e8400-e29b-41d4-a716-446655440000"),
},
}
Expand All @@ -52,6 +51,7 @@ var _ = Describe("CreateMachine", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "12345678-1234-1234-1234-123456789abc",
Region: "eu01",
}
providerSpecRaw, _ := encodeProviderSpec(providerSpec)

Expand Down
8 changes: 7 additions & 1 deletion pkg/provider/core_create_machine_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ var _ = Describe("CreateMachine", func() {
Data: map[string][]byte{
"project-id": []byte("11111111-2222-3333-4444-555555555555"),
"serviceaccount.json": []byte(`{"credentials":{"iss":"test"}}`),
"region": []byte("eu01-1"),
"networkId": []byte("770e8400-e29b-41d4-a716-446655440000"),
},
}
Expand All @@ -49,6 +48,7 @@ var _ = Describe("CreateMachine", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "12345678-1234-1234-1234-123456789abc",
Region: "eu01",
}
providerSpecRaw, _ := encodeProviderSpec(providerSpec)

Expand Down Expand Up @@ -82,6 +82,7 @@ var _ = Describe("CreateMachine", func() {
It("should pass KeypairName to API when specified", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
Region: "eu01",
ImageID: "12345678-1234-1234-1234-123456789abc",
KeypairName: "my-ssh-key",
}
Expand Down Expand Up @@ -129,6 +130,7 @@ var _ = Describe("CreateMachine", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "12345678-1234-1234-1234-123456789abc",
Region: "eu01",
AvailabilityZone: "eu01-1",
}
providerSpecRaw, _ := encodeProviderSpec(providerSpec)
Expand Down Expand Up @@ -174,6 +176,7 @@ var _ = Describe("CreateMachine", func() {
It("should pass AffinityGroup to API when specified", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
Region: "eu01",
ImageID: "12345678-1234-1234-1234-123456789abc",
AffinityGroup: "880e8400-e29b-41d4-a716-446655440000",
}
Expand Down Expand Up @@ -218,6 +221,7 @@ var _ = Describe("CreateMachine", func() {
It("should pass ServiceAccountMails to API when specified", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
Region: "eu01",
ImageID: "12345678-1234-1234-1234-123456789abc",
ServiceAccountMails: []string{
"my-service@sa.stackit.cloud",
Expand Down Expand Up @@ -268,6 +272,7 @@ var _ = Describe("CreateMachine", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
ImageID: "12345678-1234-1234-1234-123456789abc",
Region: "eu01",
Agent: &api.AgentSpec{
Provisioned: &provisioned,
},
Expand Down Expand Up @@ -314,6 +319,7 @@ var _ = Describe("CreateMachine", func() {
It("should pass Metadata to API when specified", func() {
providerSpec := &api.ProviderSpec{
MachineType: "c2i.2",
Region: "eu01",
ImageID: "12345678-1234-1234-1234-123456789abc",
Metadata: map[string]interface{}{
"environment": "production",
Expand Down
Loading