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
19 changes: 12 additions & 7 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import (
corev1 "k8s.io/api/core/v1"
)

const (
StackitProjectIDSecretKey = "project-id"
StackitServiceAccountKey = "serviceaccount.json"
)

// uuidRegex is a regex pattern for validating UUID format
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)

Expand Down Expand Up @@ -53,22 +58,22 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
return errors // Return early if secret is nil
}

projectID, ok := secrets.Data["project-id"]
projectID, ok := secrets.Data[StackitProjectIDSecretKey]
if !ok {
errors = append(errors, fmt.Errorf("secret field 'project-id' is required"))
errors = append(errors, fmt.Errorf("secret field '%s' is required", StackitProjectIDSecretKey))
} else if len(projectID) == 0 {
errors = append(errors, fmt.Errorf("secret field 'project-id' cannot be empty"))
errors = append(errors, fmt.Errorf("secret field '%s' cannot be empty", StackitProjectIDSecretKey))
} else if !isValidUUID(string(projectID)) {
errors = append(errors, fmt.Errorf("secret field 'project-id' must be a valid UUID"))
errors = append(errors, fmt.Errorf("secret field '%s' must be a valid UUID", StackitProjectIDSecretKey))
}

// Validate serviceAccountKey (required for authentication)
// ServiceAccount Key Flow: JSON string containing service account credentials and private key
serviceAccountKey, ok := secrets.Data["serviceaccount.json"]
serviceAccountKey, ok := secrets.Data[StackitServiceAccountKey]
if !ok {
errors = append(errors, fmt.Errorf("secret field 'serviceaccount.json' is required"))
errors = append(errors, fmt.Errorf("secret field '%s' is required", StackitServiceAccountKey))
} else if len(serviceAccountKey) == 0 {
errors = append(errors, fmt.Errorf("secret field 'serviceaccount.json' cannot be empty"))
errors = append(errors, fmt.Errorf("secret field '%s' cannot be empty", StackitServiceAccountKey))
} else if !isValidJSON(string(serviceAccountKey)) {
errors = append(errors, fmt.Errorf("secret field 'serviceAccountKey' must be valid JSON (service account credentials)"))
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/provider/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ 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"])
projectID, serviceAccountKey := extractSecretCredentials(req.Secret.Data)

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand Down
6 changes: 4 additions & 2 deletions pkg/provider/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR
defer klog.V(2).Infof("Machine deletion request has been processed for %q", req.Machine.Name)

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

// 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))
Expand All @@ -48,7 +49,8 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR
}

if projectID == "" {
projectID = string(req.Secret.Data["project-id"])
// use the secret as a fallback
projectID = projectIDFromSecret
}

providerSpec, err := decodeProviderSpec(req.MachineClass)
Expand Down
13 changes: 10 additions & 3 deletions pkg/provider/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/gardener/machine-controller-manager/pkg/apis/machine/v1alpha1"
api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis"
"github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis/validation"
)

// decodeProviderSpec decodes the ProviderSpec from a MachineClass
Expand All @@ -31,18 +32,18 @@ func encodeProviderSpecForResponse(spec *api.ProviderSpec) ([]byte, error) {
// parseProviderID parses a STACKIT ProviderID and extracts the projectID and serverID
// Expected format: stackit://<projectId>/<serverId>
func parseProviderID(providerID string) (projectID, serverID string, err error) {
const prefix = "stackit://"
prefix := fmt.Sprintf("%s://", StackitProviderName)

if !strings.HasPrefix(providerID, prefix) {
return "", "", fmt.Errorf("ProviderID must start with 'stackit://'")
return "", "", fmt.Errorf("ProviderID must start with '%s://'", StackitProviderName)
}

// Remove prefix and split by '/'
remainder := strings.TrimPrefix(providerID, prefix)
parts := strings.Split(remainder, "/")

if len(parts) != 2 {
return "", "", fmt.Errorf("ProviderID must have format 'stackit://<projectId>/<serverId>'")
return "", "", fmt.Errorf("ProviderID must have format '%s://<projectId>/<serverId>'", StackitProviderName)
}

if parts[0] == "" || parts[1] == "" {
Expand All @@ -51,3 +52,9 @@ func parseProviderID(providerID string) (projectID, serverID string, err error)

return parts[0], parts[1], nil
}

func extractSecretCredentials(secretData map[string][]byte) (projectID, serviceAccountKey string) {
projectID = string(secretData[validation.StackitProjectIDSecretKey])
serviceAccountKey = string(secretData[validation.StackitServiceAccountKey])
return projectID, serviceAccountKey
}
3 changes: 1 addition & 2 deletions pkg/provider/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesReq
defer klog.V(2).Infof("List machines request has been processed for %q", req.MachineClass.Name)

// Extract credentials from Secret
projectID := string(req.Secret.Data["project-id"])
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])
projectID, serviceAccountKey := extractSecretCredentials(req.Secret.Data)

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/provider/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS
}

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

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
Expand All @@ -50,7 +50,7 @@ func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineS
// Expected format: stackit://<projectId>/<serverId>
projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID)
if projectID == "" {
projectID = string(req.Secret.Data["project-id"])
projectID = projectIDFromSecret
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid ProviderID format: %v", err))
Expand Down