diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES index c23cc234..1a6eae4f 100644 --- a/OWNERS_ALIASES +++ b/OWNERS_ALIASES @@ -18,6 +18,7 @@ aliases: - jamand - breuerfelix - aniruddha2000 + machine-controller-manager-provider-stackit-approvers: - dergeberl - JuliusSte diff --git a/cmd/machine-controller/main.go b/cmd/machine-controller/main.go index f15032b8..0686b96c 100644 --- a/cmd/machine-controller/main.go +++ b/cmd/machine-controller/main.go @@ -1,24 +1,3 @@ -/* -Copyright 2014 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -This file was copied and modified from the kubernetes/kubernetes project -https://github.com/kubernetes/kubernetes/release-1.8/cmd/kube-controller-manager/controller_manager.go - -Modifications Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. -*/ - package main import ( diff --git a/hack/rename-project b/hack/rename-project index fc0300bd..265e2ffa 100755 --- a/hack/rename-project +++ b/hack/rename-project @@ -1,8 +1,4 @@ #!/bin/bash -eu -# -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -# -# SPDX-License-Identifier: Apache-2.0 project_name=$1 provider_name=$2 diff --git a/pkg/client/helper.go b/pkg/client/helper.go new file mode 100644 index 00000000..4ca29941 --- /dev/null +++ b/pkg/client/helper.go @@ -0,0 +1,57 @@ +package client + +// ptr returns a pointer to the given value +// This helper is needed because the STACKIT SDK uses pointers for optional fields +func ptr[T any](v T) *T { + return &v +} + +// convertLabelsToSDK converts map[string]string to *map[string]interface{} for SDK +// +//nolint:gocritic // SDK requires *map +func convertLabelsToSDK(labels map[string]string) *map[string]interface{} { + if labels == nil { + return nil + } + + result := make(map[string]interface{}, len(labels)) + for k, v := range labels { + result[k] = v + } + return &result +} + +// convertLabelsFromSDK converts *map[string]interface{} from SDK to map[string]string +// +//nolint:gocritic // SDK requires *map +func convertLabelsFromSDK(labels *map[string]interface{}) map[string]string { + if labels == nil { + return nil + } + + result := make(map[string]string, len(*labels)) + for k, v := range *labels { + if strVal, ok := v.(string); ok { + result[k] = strVal + } + } + return result +} + +// convertStringSliceToSDK converts []string to *[]string for SDK +func convertStringSliceToSDK(slice []string) *[]string { + if slice == nil { + return nil + } + return &slice +} + +// convertMetadataToSDK converts map[string]interface{} to *map[string]interface{} for SDK +// +//nolint:gocritic // SDK requires *map +func convertMetadataToSDK(metadata map[string]interface{}) *map[string]interface{} { + if metadata == nil { + return nil + } + return &metadata +} diff --git a/pkg/client/mock/client.go b/pkg/client/mock/client.go new file mode 100644 index 00000000..bb623de3 --- /dev/null +++ b/pkg/client/mock/client.go @@ -0,0 +1,77 @@ +package mock + +import ( + "context" + "encoding/json" + + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" +) + +// StackitClient is a mock implementation of StackitClient for testing +// Note: Single-tenant design - each client is bound to one set of credentials +type StackitClient struct { + CreateServerFunc func(ctx context.Context, projectID, region string, req *client.CreateServerRequest) (*client.Server, error) + GetServerFunc func(ctx context.Context, projectID, region, serverID string) (*client.Server, error) + DeleteServerFunc func(ctx context.Context, projectID, region, serverID string) error + ListServersFunc func(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*client.Server, error) + GetNICsFunc func(ctx context.Context, projectID, region, serverID string) ([]*client.NIC, error) + UpdateNICFunc func(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*client.NIC, error) +} + +func (m *StackitClient) CreateServer(ctx context.Context, projectID, region string, req *client.CreateServerRequest) (*client.Server, error) { + if m.CreateServerFunc != nil { + return m.CreateServerFunc(ctx, projectID, region, req) + } + return &client.Server{ + ID: "550e8400-e29b-41d4-a716-446655440000", + Name: req.Name, + Status: "CREATING", + }, nil +} + +func (m *StackitClient) GetServer(ctx context.Context, projectID, region, serverID string) (*client.Server, error) { + if m.GetServerFunc != nil { + return m.GetServerFunc(ctx, projectID, region, serverID) + } + return &client.Server{ + ID: serverID, + Name: "test-machine", + Status: "ACTIVE", + }, nil +} + +func (m *StackitClient) DeleteServer(ctx context.Context, projectID, region, serverID string) error { + if m.DeleteServerFunc != nil { + return m.DeleteServerFunc(ctx, projectID, region, serverID) + } + return nil +} + +func (m *StackitClient) ListServers(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*client.Server, error) { + if m.ListServersFunc != nil { + return m.ListServersFunc(ctx, projectID, region, labelSelector) + } + return []*client.Server{}, nil +} + +func (m *StackitClient) GetNICsForServer(ctx context.Context, projectID, region, serverID string) ([]*client.NIC, error) { + if m.GetNICsFunc != nil { + return m.GetNICsFunc(ctx, projectID, region, serverID) + } + return []*client.NIC{}, nil +} + +func (m *StackitClient) UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*client.NIC, error) { + if m.UpdateNICFunc != nil { + return m.UpdateNICFunc(ctx, projectID, region, networkID, nicID, allowedAddresses) + } + return &client.NIC{}, nil +} + +// UpdateNIC updates a network interface + +// encodeProviderSpec is a helper function to encode ProviderSpec for tests +func EncodeProviderSpec(spec *api.ProviderSpec) ([]byte, error) { + return json.Marshal(spec) +} diff --git a/pkg/provider/sdk_client.go b/pkg/client/sdk.go similarity index 98% rename from pkg/provider/sdk_client.go rename to pkg/client/sdk.go index e63c0cc5..4584d665 100644 --- a/pkg/provider/sdk_client.go +++ b/pkg/client/sdk.go @@ -1,8 +1,4 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package provider +package client import ( "context" diff --git a/pkg/provider/sdk_client_test.go b/pkg/client/sdk_test.go similarity index 98% rename from pkg/provider/sdk_client_test.go rename to pkg/client/sdk_test.go index 3b1da0a8..40f9da68 100644 --- a/pkg/provider/sdk_client_test.go +++ b/pkg/client/sdk_test.go @@ -1,8 +1,4 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package provider +package client import ( "errors" diff --git a/pkg/provider/stackit_client.go b/pkg/client/stackit.go similarity index 95% rename from pkg/provider/stackit_client.go rename to pkg/client/stackit.go index 0fe44be9..ad83be08 100644 --- a/pkg/provider/stackit_client.go +++ b/pkg/client/stackit.go @@ -1,8 +1,4 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package provider +package client import ( "context" @@ -18,7 +14,6 @@ import ( // // Note: region parameter is required by STACKIT SDK v1.0.0+ // It must be extracted from the Secret (e.g., "eu01-1", "eu01-2") -// nolint:dupl // the duplicates are mock functions type StackitClient interface { // CreateServer creates a new server in STACKIT CreateServer(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) diff --git a/pkg/provider/apis/provider_spec.go b/pkg/provider/apis/provider_spec.go index cb1822d4..e05d82b4 100644 --- a/pkg/provider/apis/provider_spec.go +++ b/pkg/provider/apis/provider_spec.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package api // ProviderSpec is the spec to be used while parsing the calls. diff --git a/pkg/provider/apis/validation/validation.go b/pkg/provider/apis/validation/validation.go index bc7a7f55..a9ecbf2d 100644 --- a/pkg/provider/apis/validation/validation.go +++ b/pkg/provider/apis/validation/validation.go @@ -1,8 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -// Package validation - validation is used to validate cloud specific ProviderSpec package validation import ( diff --git a/pkg/provider/apis/validation/validation_core_labels_test.go b/pkg/provider/apis/validation/validation_core_labels_test.go index 2e59f1bc..eb5a926f 100644 --- a/pkg/provider/apis/validation/validation_core_labels_test.go +++ b/pkg/provider/apis/validation/validation_core_labels_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/apis/validation/validation_fields_test.go b/pkg/provider/apis/validation/validation_fields_test.go index 7ceb3aba..4c3a1812 100644 --- a/pkg/provider/apis/validation/validation_fields_test.go +++ b/pkg/provider/apis/validation/validation_fields_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/apis/validation/validation_networking_test.go b/pkg/provider/apis/validation/validation_networking_test.go index beb22110..fc7cf93c 100644 --- a/pkg/provider/apis/validation/validation_networking_test.go +++ b/pkg/provider/apis/validation/validation_networking_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/apis/validation/validation_secgroup_test.go b/pkg/provider/apis/validation/validation_secgroup_test.go index 5477366b..89d97e83 100644 --- a/pkg/provider/apis/validation/validation_secgroup_test.go +++ b/pkg/provider/apis/validation/validation_secgroup_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/apis/validation/validation_secret_test.go b/pkg/provider/apis/validation/validation_secret_test.go index 89cb9773..dc4dda88 100644 --- a/pkg/provider/apis/validation/validation_secret_test.go +++ b/pkg/provider/apis/validation/validation_secret_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/apis/validation/validation_volumes_test.go b/pkg/provider/apis/validation/validation_volumes_test.go index 4576c4be..9c74f595 100644 --- a/pkg/provider/apis/validation/validation_volumes_test.go +++ b/pkg/provider/apis/validation/validation_volumes_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package validation_test import ( diff --git a/pkg/provider/core.go b/pkg/provider/core.go index 0dcad840..d5020049 100644 --- a/pkg/provider/core.go +++ b/pkg/provider/core.go @@ -1,23 +1,11 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -// Package provider contains the cloud provider specific implementations to manage machines package provider import ( "context" - "encoding/base64" - "errors" - "fmt" - "slices" - "strings" "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" - api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" - "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis/validation" "k8s.io/klog/v2" ) @@ -28,475 +16,6 @@ const ( StackitRoleLabel = "mcm.gardener.cloud/role" ) -// CreateMachine handles a machine creation request by creating a STACKIT server -// -// This method creates a new server in STACKIT infrastructure based on the ProviderSpec -// configuration in the MachineClass. It assigns MCM-specific labels to the server for -// tracking and orphan VM detection. -// -// Returns: -// - ProviderID: Unique identifier in format "stackit:///" -// - NodeName: Name that the VM will register with in Kubernetes (matches Machine name) -// -// Error codes: -// - InvalidArgument: Invalid ProviderSpec or missing required fields -// - Internal: Failed to create server or communicate with STACKIT API -// -//nolint:gocyclo,funlen//TODO:refactor -func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineRequest) (*driver.CreateMachineResponse, error) { - // Log messages to track request - klog.V(2).Infof("Machine creation request has been received for %q", req.Machine.Name) - defer klog.V(2).Infof("Machine creation request has been processed for %q", req.Machine.Name) - - // Check if incoming provider in the MachineClass is a provider we support - if req.MachineClass.Provider != StackitProviderName { - err := fmt.Errorf("requested for Provider '%s', we only support '%s'", req.MachineClass.Provider, StackitProviderName) - return nil, status.Error(codes.InvalidArgument, err.Error()) - } - - // Decode ProviderSpec from MachineClass - providerSpec, err := decodeProviderSpec(req.MachineClass) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - - // Validate ProviderSpec and Secret - validationErrs := validation.ValidateProviderSpecNSecret(providerSpec, req.Secret) - if len(validationErrs) > 0 { - return nil, status.Error(codes.InvalidArgument, validationErrs[0].Error()) - } - - // Extract credentials from Secret - projectID := string(req.Secret.Data["project-id"]) - serviceAccountKey := string(req.Secret.Data["serviceaccount.json"]) - - // 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)) - } - - // 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 - } - } - // 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 := &CreateServerRequest{ - Name: req.Machine.Name, - MachineType: providerSpec.MachineType, - ImageID: providerSpec.ImageID, - Labels: labels, - } - - // Add networking configuration (required in v2 API) - // If not specified in ProviderSpec, try to use networkId from Secret, or use empty - if providerSpec.Networking != nil { - createReq.Networking = &ServerNetworkingRequest{ - NetworkID: providerSpec.Networking.NetworkID, - NICIDs: providerSpec.Networking.NICIDs, - } - } else { - // v2 API requires networking field - use networkId from Secret if available - // This allows tests/deployments to specify a default network without modifying each MachineClass - networkID := string(req.Secret.Data["networkId"]) - createReq.Networking = &ServerNetworkingRequest{ - NetworkID: networkID, // Can be empty string if not in Secret - } - } - - // Add security groups if specified - if len(providerSpec.SecurityGroups) > 0 { - createReq.SecurityGroups = providerSpec.SecurityGroups - } - - // Add userData for VM bootstrapping - // Priority: ProviderSpec.UserData > Secret.userData - // Note: IAAS API requires base64-encoded userData (OpenAPI spec: format=byte) - var userDataPlain string - if providerSpec.UserData != "" { - userDataPlain = providerSpec.UserData - } else if userData, ok := req.Secret.Data["userData"]; ok && len(userData) > 0 { - userDataPlain = string(userData) - } - - if userDataPlain != "" { - createReq.UserData = base64.StdEncoding.EncodeToString([]byte(userDataPlain)) - } - - // Add boot volume configuration if specified - if providerSpec.BootVolume != nil { - createReq.BootVolume = &BootVolumeRequest{ - DeleteOnTermination: providerSpec.BootVolume.DeleteOnTermination, - PerformanceClass: providerSpec.BootVolume.PerformanceClass, - Size: providerSpec.BootVolume.Size, - } - - // Add boot volume source if specified - if providerSpec.BootVolume.Source != nil { - createReq.BootVolume.Source = &BootVolumeSourceRequest{ - Type: providerSpec.BootVolume.Source.Type, - ID: providerSpec.BootVolume.Source.ID, - } - } - } - - // Add additional volumes if specified - if len(providerSpec.Volumes) > 0 { - createReq.Volumes = providerSpec.Volumes - } - - // Add keypair name if specified - if providerSpec.KeypairName != "" { - createReq.KeypairName = providerSpec.KeypairName - } - - // Add availability zone if specified - if providerSpec.AvailabilityZone != "" { - createReq.AvailabilityZone = providerSpec.AvailabilityZone - } - - // Add affinity group if specified - if providerSpec.AffinityGroup != "" { - createReq.AffinityGroup = providerSpec.AffinityGroup - } - - // Add service account mails if specified - if len(providerSpec.ServiceAccountMails) > 0 { - createReq.ServiceAccountMails = providerSpec.ServiceAccountMails - } - - // Add agent configuration if specified - if providerSpec.Agent != nil { - createReq.Agent = &AgentRequest{ - Provisioned: providerSpec.Agent.Provisioned, - } - } - - // Add metadata if specified - if len(providerSpec.Metadata) > 0 { - createReq.Metadata = providerSpec.Metadata - } - - // check if server already exists - server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) - if err != nil { - klog.Errorf("Failed to fetch server for machine %q: %v", req.Machine.Name, err) - return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to fetch server: %v", err)) - } - - if server == nil { - // Call STACKIT API to create server - 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.Unavailable, fmt.Sprintf("failed to create server: %v", err)) - } - } - - if err := p.patchNetworkInterface(ctx, projectID, server.ID, providerSpec); err != nil { - klog.Errorf("Failed to patch network interface for server %q: %v", req.Machine.Name, err) - return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch network interface for server: %v", err)) - } - - // Generate ProviderID in format: stackit:/// - providerID := fmt.Sprintf("%s://%s/%s", StackitProviderName, projectID, server.ID) - klog.V(2).Infof("Successfully created server %q with ID %q for machine %q", server.Name, server.ID, req.Machine.Name) - - return &driver.CreateMachineResponse{ - ProviderID: providerID, - NodeName: req.Machine.Name, - }, nil -} - -func (p *Provider) getServerByName(ctx context.Context, projectID, region, serverName string) (*Server, error) { - // Check if the server got already created - labelSelector := map[string]string{ - StackitMachineLabel: serverName, - } - servers, err := p.client.ListServers(ctx, projectID, region, labelSelector) - if err != nil { - return nil, fmt.Errorf("SDK ListServers with labelSelector: %v failed: %w", labelSelector, err) - } - - if len(servers) > 1 { - return nil, fmt.Errorf("%v servers found for server name %v", len(servers), serverName) - } - - if len(servers) == 1 { - return servers[0], nil - } - - // no servers found len == 0 - return nil, nil -} - -func (p *Provider) patchNetworkInterface(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) error { - if len(providerSpec.AllowedAddresses) == 0 { - return nil - } - - nics, err := p.client.GetNICsForServer(ctx, projectID, providerSpec.Region, serverID) - if err != nil { - return fmt.Errorf("failed to get NICs for server %q: %w", serverID, err) - } - - if len(nics) == 0 { - return fmt.Errorf("failed to find NIC for server %q", serverID) - } - - for _, nic := range nics { - // if networking is not set, server is inside the default network - // just patch the interface since the server should only have one - if providerSpec.Networking != nil { - // only process interfaces that are either in the configured network (NetworkID) or are defined in NICIDs - if providerSpec.Networking.NetworkID != nic.NetworkID && !slices.Contains(providerSpec.Networking.NICIDs, nic.ID) { - continue - } - } - - updateNic := false - // check if every cidr in providerspec.allowedAddresses is inside the nic allowedAddresses - for _, allowedAddress := range providerSpec.AllowedAddresses { - if !slices.Contains(nic.AllowedAddresses, allowedAddress) { - nic.AllowedAddresses = append(nic.AllowedAddresses, allowedAddress) - updateNic = true - } - } - - if !updateNic { - continue - } - - if _, err := p.client.UpdateNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID, nic.AllowedAddresses); err != nil { - return fmt.Errorf("failed to update allowed addresses for NIC %s: %w", nic.ID, err) - } - - klog.V(2).Infof("Updated allowed addresses for NIC %s to %v", nic.ID, nic.AllowedAddresses) - } - - return nil -} - -// DeleteMachine handles a machine deletion request by deleting the STACKIT server -// -// This method deletes the server identified by the ProviderID from STACKIT infrastructure. -// It is idempotent - if the server is already deleted (404), it returns success. -// -// Error codes: -// - InvalidArgument: Missing or invalid ProviderID -// - Internal: Failed to delete server or communicate with STACKIT API -func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineRequest) (*driver.DeleteMachineResponse, error) { - // Log messages to track delete request - klog.V(2).Infof("Machine deletion request has been received for %q", req.Machine.Name) - 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"]) - // 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)) - } - - var projectID, serverID string - var err error - if req.Machine.Spec.ProviderID != "" { - if !strings.HasPrefix(req.Machine.Spec.ProviderID, StackitProviderName) { - return nil, status.Error(codes.InvalidArgument, "providerID is not empty and does not start with stackit://") - } - - // Parse ProviderID to extract projectID and serverID - projectID, serverID, err = parseProviderID(req.Machine.Spec.ProviderID) - if err != nil { - klog.V(2).Infof("invalid ProviderID format: %v", err) - } - } - - if projectID == "" { - projectID = string(req.Secret.Data["project-id"]) - } - - providerSpec, err := decodeProviderSpec(req.MachineClass) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - - if serverID == "" { - server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) - if err != nil { - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to find server by name: %v", err)) - } - - if server != nil { - serverID = server.ID - } - } - - if serverID == "" { - klog.V(2).Infof("Server is already deleted for machine %q", req.Machine.Name) - return &driver.DeleteMachineResponse{}, nil - } - - // Call STACKIT API to delete server - 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) { - klog.V(2).Infof("Server %q already deleted for machine %q (idempotent)", serverID, req.Machine.Name) - return &driver.DeleteMachineResponse{}, nil - } - // All other errors are internal errors - klog.Errorf("Failed to delete server for machine %q: %v", req.Machine.Name, err) - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to delete server: %v", err)) - } - - klog.V(2).Infof("Successfully deleted server %q for machine %q", serverID, req.Machine.Name) - - return &driver.DeleteMachineResponse{}, nil -} - -// GetMachineStatus retrieves the current status of a STACKIT server -// -// This method queries STACKIT API to get the current state of the server identified -// by the Machine's ProviderID. If the ProviderID is empty (machine not created yet) -// or the server doesn't exist, it returns NotFound error. -// -// Returns: -// - ProviderID: The machine's ProviderID -// - NodeName: Name that the VM registered with in Kubernetes -// -// Error codes: -// - NotFound: Machine has no ProviderID yet, or server not found in STACKIT -// - InvalidArgument: Invalid ProviderID format -// - Internal: Failed to get server status or communicate with STACKIT API -func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineStatusRequest) (*driver.GetMachineStatusResponse, error) { - // Log messages to track start and end of request - klog.V(2).Infof("Get request has been received for %q", req.Machine.Name) - defer klog.V(2).Infof("Machine get request has been processed successfully for %q", req.Machine.Name) - - // When ProviderID is empty, the machine doesn't exist yet - // Return NotFound so MCM knows to call CreateMachine - if req.Machine.Spec.ProviderID == "" { - klog.V(2).Infof("Machine %q has no ProviderID, returning NotFound", req.Machine.Name) - return nil, status.Error(codes.NotFound, "machine does not have a ProviderID yet") - } - - // Extract credentials from Secret - serviceAccountKey := string(req.Secret.Data["serviceaccount.json"]) - - // 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)) - } - - // Parse ProviderID to extract projectID and serverID - // Expected format: stackit:/// - projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID) - if projectID == "" { - projectID = string(req.Secret.Data["project-id"]) - } - if err != nil { - 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, providerSpec.Region, serverID) - if err != nil { - // Check if server was not found (404) - if errors.Is(err, ErrServerNotFound) { - klog.V(2).Infof("Server %q not found for machine %q", serverID, req.Machine.Name) - return nil, status.Error(codes.NotFound, fmt.Sprintf("server %q not found", serverID)) - } - // All other errors are internal errors - klog.Errorf("Failed to get server status for machine %q: %v", req.Machine.Name, err) - return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get server status: %v", err)) - } - - klog.V(2).Infof("Retrieved server status for machine %q: status=%s", req.Machine.Name, server.Status) - - return &driver.GetMachineStatusResponse{ - ProviderID: req.Machine.Spec.ProviderID, - NodeName: req.Machine.Name, - }, nil -} - -// 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 -// to detect and clean up orphan VMs that are not backed by Machine CRs. -// -// Returns: -// - MachineList: Map of ProviderID to MachineName for all servers matching the MachineClass -// -// Error codes: -// - Internal: Failed to list servers or communicate with STACKIT API -func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesRequest) (*driver.ListMachinesResponse, error) { - // Log messages to track start and end of request - klog.V(2).Infof("List machines request has been received for %q", req.MachineClass.Name) - 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"]) - - // 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 - labelSelector := map[string]string{ - StackitMachineClassLabel: req.MachineClass.Name, - } - servers, err := p.client.ListServers(ctx, projectID, providerSpec.Region, labelSelector) - 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)) - } - - // Filter servers by MachineClass label - // We use the "mcm.gardener.cloud/machineclass" label to identify which servers belong to this MachineClass - machineList := make(map[string]string) - for _, server := range servers { - // Generate ProviderID in format: stackit:/// - providerID := fmt.Sprintf("stackit://%s/%s", projectID, server.ID) - - // Get machine name from labels (fallback to server name if not found) - machineName := server.Name - if machineLabel, ok := server.Labels[StackitMachineLabel]; ok { - machineName = machineLabel - } - - machineList[providerID] = machineName - } - - klog.V(2).Infof("Found %d machines for MachineClass %q", len(machineList), req.MachineClass.Name) - - return &driver.ListMachinesResponse{ - MachineList: machineList, - }, nil -} - // GetVolumeIDs extracts volume IDs from PersistentVolume specs // // This method is used by MCM to get volume IDs for persistent volumes. diff --git a/pkg/provider/core_mocks_test.go b/pkg/provider/core_mocks_test.go deleted file mode 100644 index ce4388c8..00000000 --- a/pkg/provider/core_mocks_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package provider - -import ( - "context" - - api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" -) - -// mockStackitClient is a mock implementation of StackitClient for testing -// Note: Single-tenant design - each client is bound to one set of credentials -type mockStackitClient struct { - createServerFunc func(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) - getServerFunc func(ctx context.Context, projectID, region, serverID string) (*Server, error) - deleteServerFunc func(ctx context.Context, projectID, region, serverID string) error - listServersFunc func(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*Server, error) - getNICsFunc func(ctx context.Context, projectID, region, serverID string) ([]*NIC, error) - updateNICFunc func(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*NIC, error) -} - -func (m *mockStackitClient) CreateServer(ctx context.Context, projectID, region string, req *CreateServerRequest) (*Server, error) { - if m.createServerFunc != nil { - return m.createServerFunc(ctx, projectID, region, req) - } - return &Server{ - ID: "550e8400-e29b-41d4-a716-446655440000", - Name: req.Name, - Status: "CREATING", - }, nil -} - -func (m *mockStackitClient) GetServer(ctx context.Context, projectID, region, serverID string) (*Server, error) { - if m.getServerFunc != nil { - return m.getServerFunc(ctx, projectID, region, serverID) - } - return &Server{ - ID: serverID, - Name: "test-machine", - Status: "ACTIVE", - }, nil -} - -func (m *mockStackitClient) DeleteServer(ctx context.Context, projectID, region, serverID string) error { - if m.deleteServerFunc != nil { - return m.deleteServerFunc(ctx, projectID, region, serverID) - } - return nil -} - -func (m *mockStackitClient) ListServers(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*Server, error) { - if m.listServersFunc != nil { - return m.listServersFunc(ctx, projectID, region, labelSelector) - } - return []*Server{}, nil -} - -func (m *mockStackitClient) GetNICsForServer(ctx context.Context, projectID, region, serverID string) ([]*NIC, error) { - if m.getNICsFunc != nil { - return m.getNICsFunc(ctx, projectID, region, serverID) - } - return []*NIC{}, nil -} - -func (m *mockStackitClient) UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*NIC, error) { - if m.updateNICFunc != nil { - return m.updateNICFunc(ctx, projectID, region, networkID, nicID, allowedAddresses) - } - return &NIC{}, nil -} - -// UpdateNIC updates a network interface - -// encodeProviderSpec is a helper function to encode ProviderSpec for tests -func encodeProviderSpec(spec *api.ProviderSpec) ([]byte, error) { - return encodeProviderSpecForResponse(spec) -} diff --git a/pkg/provider/create.go b/pkg/provider/create.go new file mode 100644 index 00000000..ad6fdc01 --- /dev/null +++ b/pkg/provider/create.go @@ -0,0 +1,273 @@ +package provider + +import ( + "context" + "encoding/base64" + "fmt" + "slices" + + "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis/validation" + "k8s.io/klog/v2" +) + +// CreateMachine handles a machine creation request by creating a STACKIT server +// +// This method creates a new server in STACKIT infrastructure based on the ProviderSpec +// configuration in the MachineClass. It assigns MCM-specific labels to the server for +// tracking and orphan VM detection. +// +// Returns: +// - ProviderID: Unique identifier in format "stackit:///" +// - NodeName: Name that the VM will register with in Kubernetes (matches Machine name) +// +// Error codes: +// - InvalidArgument: Invalid ProviderSpec or missing required fields +// - Internal: Failed to create server or communicate with STACKIT API +// +//nolint:gocyclo,funlen//TODO:refactor +func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineRequest) (*driver.CreateMachineResponse, error) { + // Log messages to track request + klog.V(2).Infof("Machine creation request has been received for %q", req.Machine.Name) + defer klog.V(2).Infof("Machine creation request has been processed for %q", req.Machine.Name) + + // Check if incoming provider in the MachineClass is a provider we support + if req.MachineClass.Provider != StackitProviderName { + err := fmt.Errorf("requested for Provider '%s', we only support '%s'", req.MachineClass.Provider, StackitProviderName) + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + // Decode ProviderSpec from MachineClass + providerSpec, err := decodeProviderSpec(req.MachineClass) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + // Validate ProviderSpec and Secret + validationErrs := validation.ValidateProviderSpecNSecret(providerSpec, req.Secret) + if len(validationErrs) > 0 { + return nil, status.Error(codes.InvalidArgument, validationErrs[0].Error()) + } + + // Extract credentials from Secret + projectID := string(req.Secret.Data["project-id"]) + serviceAccountKey := string(req.Secret.Data["serviceaccount.json"]) + + // 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)) + } + + // 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 + } + } + // 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{ + Name: req.Machine.Name, + MachineType: providerSpec.MachineType, + ImageID: providerSpec.ImageID, + Labels: labels, + } + + // Add networking configuration (required in v2 API) + // If not specified in ProviderSpec, try to use networkId from Secret, or use empty + if providerSpec.Networking != nil { + createReq.Networking = &client.ServerNetworkingRequest{ + NetworkID: providerSpec.Networking.NetworkID, + NICIDs: providerSpec.Networking.NICIDs, + } + } else { + // v2 API requires networking field - use networkId from Secret if available + // This allows tests/deployments to specify a default network without modifying each MachineClass + networkID := string(req.Secret.Data["networkId"]) + createReq.Networking = &client.ServerNetworkingRequest{ + NetworkID: networkID, // Can be empty string if not in Secret + } + } + + // Add security groups if specified + if len(providerSpec.SecurityGroups) > 0 { + createReq.SecurityGroups = providerSpec.SecurityGroups + } + + // Add userData for VM bootstrapping + // Priority: ProviderSpec.UserData > Secret.userData + // Note: IAAS API requires base64-encoded userData (OpenAPI spec: format=byte) + var userDataPlain string + if providerSpec.UserData != "" { + userDataPlain = providerSpec.UserData + } else if userData, ok := req.Secret.Data["userData"]; ok && len(userData) > 0 { + userDataPlain = string(userData) + } + + if userDataPlain != "" { + createReq.UserData = base64.StdEncoding.EncodeToString([]byte(userDataPlain)) + } + + // Add boot volume configuration if specified + if providerSpec.BootVolume != nil { + createReq.BootVolume = &client.BootVolumeRequest{ + DeleteOnTermination: providerSpec.BootVolume.DeleteOnTermination, + PerformanceClass: providerSpec.BootVolume.PerformanceClass, + Size: providerSpec.BootVolume.Size, + } + + // Add boot volume source if specified + if providerSpec.BootVolume.Source != nil { + createReq.BootVolume.Source = &client.BootVolumeSourceRequest{ + Type: providerSpec.BootVolume.Source.Type, + ID: providerSpec.BootVolume.Source.ID, + } + } + } + + // Add additional volumes if specified + if len(providerSpec.Volumes) > 0 { + createReq.Volumes = providerSpec.Volumes + } + + // Add keypair name if specified + if providerSpec.KeypairName != "" { + createReq.KeypairName = providerSpec.KeypairName + } + + // Add availability zone if specified + if providerSpec.AvailabilityZone != "" { + createReq.AvailabilityZone = providerSpec.AvailabilityZone + } + + // Add affinity group if specified + if providerSpec.AffinityGroup != "" { + createReq.AffinityGroup = providerSpec.AffinityGroup + } + + // Add service account mails if specified + if len(providerSpec.ServiceAccountMails) > 0 { + createReq.ServiceAccountMails = providerSpec.ServiceAccountMails + } + + // Add agent configuration if specified + if providerSpec.Agent != nil { + createReq.Agent = &client.AgentRequest{ + Provisioned: providerSpec.Agent.Provisioned, + } + } + + // Add metadata if specified + if len(providerSpec.Metadata) > 0 { + createReq.Metadata = providerSpec.Metadata + } + + // check if server already exists + server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) + if err != nil { + klog.Errorf("Failed to fetch server for machine %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to fetch server: %v", err)) + } + + if server == nil { + // Call STACKIT API to create server + 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.Unavailable, fmt.Sprintf("failed to create server: %v", err)) + } + } + + if err := p.patchNetworkInterface(ctx, projectID, server.ID, providerSpec); err != nil { + klog.Errorf("Failed to patch network interface for server %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch network interface for server: %v", err)) + } + + // Generate ProviderID in format: stackit:/// + providerID := fmt.Sprintf("%s://%s/%s", StackitProviderName, projectID, server.ID) + klog.V(2).Infof("Successfully created server %q with ID %q for machine %q", server.Name, server.ID, req.Machine.Name) + + return &driver.CreateMachineResponse{ + ProviderID: providerID, + NodeName: req.Machine.Name, + }, nil +} + +func (p *Provider) getServerByName(ctx context.Context, projectID, region, serverName string) (*client.Server, error) { + // Check if the server got already created + labelSelector := map[string]string{ + StackitMachineLabel: serverName, + } + servers, err := p.client.ListServers(ctx, projectID, region, labelSelector) + if err != nil { + return nil, fmt.Errorf("SDK ListServers with labelSelector: %v failed: %w", labelSelector, err) + } + + if len(servers) > 1 { + return nil, fmt.Errorf("%v servers found for server name %v", len(servers), serverName) + } + + if len(servers) == 1 { + return servers[0], nil + } + + // no servers found len == 0 + return nil, nil +} + +func (p *Provider) patchNetworkInterface(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) error { + if len(providerSpec.AllowedAddresses) == 0 { + return nil + } + + nics, err := p.client.GetNICsForServer(ctx, projectID, providerSpec.Region, serverID) + if err != nil { + return fmt.Errorf("failed to get NICs for server %q: %w", serverID, err) + } + + if len(nics) == 0 { + return fmt.Errorf("failed to find NIC for server %q", serverID) + } + + for _, nic := range nics { + // if networking is not set, server is inside the default network + // just patch the interface since the server should only have one + if providerSpec.Networking != nil { + // only process interfaces that are either in the configured network (NetworkID) or are defined in NICIDs + if providerSpec.Networking.NetworkID != nic.NetworkID && !slices.Contains(providerSpec.Networking.NICIDs, nic.ID) { + continue + } + } + + updateNic := false + // check if every cidr in providerspec.allowedAddresses is inside the nic allowedAddresses + for _, allowedAddress := range providerSpec.AllowedAddresses { + if !slices.Contains(nic.AllowedAddresses, allowedAddress) { + nic.AllowedAddresses = append(nic.AllowedAddresses, allowedAddress) + updateNic = true + } + } + + if !updateNic { + continue + } + + if _, err := p.client.UpdateNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID, nic.AllowedAddresses); err != nil { + return fmt.Errorf("failed to update allowed addresses for NIC %s: %w", nic.ID, err) + } + + klog.V(2).Infof("Updated allowed addresses for NIC %s to %v", nic.ID, nic.AllowedAddresses) + } + + return nil +} diff --git a/pkg/provider/core_create_machine_basic_test.go b/pkg/provider/create_basic_test.go similarity index 86% rename from pkg/provider/core_create_machine_basic_test.go rename to pkg/provider/create_basic_test.go index 1fc72358..952ad3f3 100644 --- a/pkg/provider/core_create_machine_basic_test.go +++ b/pkg/provider/create_basic_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -14,6 +10,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,7 +22,7 @@ var _ = Describe("CreateMachine", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.CreateMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -33,7 +31,7 @@ var _ = Describe("CreateMachine", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -53,7 +51,7 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -93,13 +91,13 @@ var _ = Describe("CreateMachine", func() { }) It("should call STACKIT API with correct parameters", func() { - var capturedReq *CreateServerRequest + var capturedReq *client.CreateServerRequest var capturedProjectID string - mockClient.createServerFunc = func(_ context.Context, projectID, _ string, req *CreateServerRequest) (*Server, error) { + mockClient.CreateServerFunc = func(_ context.Context, projectID, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedProjectID = projectID capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -123,7 +121,7 @@ var _ = Describe("CreateMachine", func() { MachineType: "", ImageID: "12345678-1234-1234-1234-123456789abc", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw _, err := provider.CreateMachine(ctx, req) @@ -139,7 +137,7 @@ var _ = Describe("CreateMachine", func() { MachineType: "c2i.2", ImageID: "", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw _, err := provider.CreateMachine(ctx, req) @@ -175,7 +173,7 @@ var _ = Describe("CreateMachine", func() { Context("when STACKIT API fails", func() { It("should return Internal error on API failure", func() { - mockClient.createServerFunc = func(_ context.Context, _, _ string, _ *CreateServerRequest) (*Server, error) { + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, _ *client.CreateServerRequest) (*client.Server, error) { return nil, fmt.Errorf("API connection failed") } diff --git a/pkg/provider/core_create_machine_config_test.go b/pkg/provider/create_config_test.go similarity index 72% rename from pkg/provider/core_create_machine_config_test.go rename to pkg/provider/create_config_test.go index 7a9c7469..686b37ac 100644 --- a/pkg/provider/core_create_machine_config_test.go +++ b/pkg/provider/create_config_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -11,6 +7,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,7 +19,7 @@ var _ = Describe("CreateMachine", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.CreateMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -30,7 +28,7 @@ var _ = Describe("CreateMachine", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -50,7 +48,7 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -87,13 +85,13 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", KeypairName: "my-ssh-key", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -108,10 +106,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send KeypairName when empty", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -134,13 +132,13 @@ var _ = Describe("CreateMachine", func() { Region: "eu01", AvailabilityZone: "eu01-1", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -155,10 +153,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send AvailabilityZone when empty", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -181,13 +179,13 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", AffinityGroup: "880e8400-e29b-41d4-a716-446655440000", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -202,10 +200,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send AffinityGroup when empty", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -228,13 +226,13 @@ var _ = Describe("CreateMachine", func() { "my-service@sa.stackit.cloud", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -251,10 +249,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send ServiceAccountMails when empty", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -278,13 +276,13 @@ var _ = Describe("CreateMachine", func() { Provisioned: &provisioned, }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -300,10 +298,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send Agent when nil", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -328,13 +326,13 @@ var _ = Describe("CreateMachine", func() { "count": 42, }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -354,10 +352,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send Metadata when nil", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", diff --git a/pkg/provider/core_create_machine_networking_test.go b/pkg/provider/create_networking_test.go similarity index 80% rename from pkg/provider/core_create_machine_networking_test.go rename to pkg/provider/create_networking_test.go index b8649d93..ce886fb7 100644 --- a/pkg/provider/core_create_machine_networking_test.go +++ b/pkg/provider/create_networking_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -11,6 +7,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,7 +19,7 @@ var _ = Describe("CreateMachine - Networking", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.CreateMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -30,7 +28,7 @@ var _ = Describe("CreateMachine - Networking", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -62,7 +60,7 @@ var _ = Describe("CreateMachine - Networking", func() { NetworkID: "770e8400-e29b-41d4-a716-446655440000", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -80,10 +78,10 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -110,7 +108,7 @@ var _ = Describe("CreateMachine - Networking", func() { }, }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -128,10 +126,10 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -160,7 +158,7 @@ var _ = Describe("CreateMachine - Networking", func() { "10.0.0.1/8", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -178,18 +176,18 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", }, nil } - mockClient.getNICsFunc = func(_ context.Context, _, _, _ string) ([]*NIC, error) { - return []*NIC{{ + mockClient.GetNICsFunc = func(_ context.Context, _, _, _ string) ([]*client.NIC, error) { + return []*client.NIC{{ ID: "990e8400-e29b-41d4-a716-446655440002", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: []string{}, @@ -197,11 +195,11 @@ var _ = Describe("CreateMachine - Networking", func() { } var called = false - mockClient.updateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*NIC, error) { + mockClient.UpdateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*client.NIC, error) { called = true Expect(addresses).To(HaveLen(1)) Expect(addresses[0]).To(Equal("10.0.0.1/8")) - return &NIC{ + return &client.NIC{ ID: "990e8400-e29b-41d4-a716-446655440002", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: addresses, @@ -231,7 +229,7 @@ var _ = Describe("CreateMachine - Networking", func() { "10.0.0.1/8", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -249,18 +247,18 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", }, nil } - mockClient.getNICsFunc = func(_ context.Context, _, _, _ string) ([]*NIC, error) { - return []*NIC{{ + mockClient.GetNICsFunc = func(_ context.Context, _, _, _ string) ([]*client.NIC, error) { + return []*client.NIC{{ ID: "880e8400-e29b-41d4-a716-446655440001", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: []string{}, @@ -268,11 +266,11 @@ var _ = Describe("CreateMachine - Networking", func() { } var called = false - mockClient.updateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*NIC, error) { + mockClient.UpdateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*client.NIC, error) { called = true Expect(addresses).To(HaveLen(1)) Expect(addresses[0]).To(Equal("10.0.0.1/8")) - return &NIC{ + return &client.NIC{ ID: "880e8400-e29b-41d4-a716-446655440001", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: addresses, @@ -299,7 +297,7 @@ var _ = Describe("CreateMachine - Networking", func() { "10.0.0.1/8", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -317,18 +315,18 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", }, nil } - mockClient.getNICsFunc = func(_ context.Context, _, _, _ string) ([]*NIC, error) { - return []*NIC{{ + mockClient.GetNICsFunc = func(_ context.Context, _, _, _ string) ([]*client.NIC, error) { + return []*client.NIC{{ ID: "990e8400-e29b-41d4-a716-446655440002", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: []string{"10.0.0.1/8"}, @@ -336,9 +334,9 @@ var _ = Describe("CreateMachine - Networking", func() { } var called = false - mockClient.updateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*NIC, error) { + mockClient.UpdateNICFunc = func(_ context.Context, _, _, _, _ string, addresses []string) (*client.NIC, error) { called = true - return &NIC{ + return &client.NIC{ ID: "990e8400-e29b-41d4-a716-446655440002", NetworkID: "770e8400-e29b-41d4-a716-446655440000", AllowedAddresses: addresses, @@ -365,7 +363,7 @@ var _ = Describe("CreateMachine - Networking", func() { Region: "eu01", // Networking is nil - should fall back to Secret } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -383,10 +381,10 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -408,7 +406,7 @@ var _ = Describe("CreateMachine - Networking", func() { Region: "eu01", // Networking is nil } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -426,10 +424,10 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, // No networkId in secret } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -458,7 +456,7 @@ var _ = Describe("CreateMachine - Networking", func() { NetworkID: "990e8400-e29b-41d4-a716-446655440002", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ @@ -476,10 +474,10 @@ var _ = Describe("CreateMachine - Networking", func() { Secret: secret, } - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -506,7 +504,7 @@ var _ = Describe("CreateMachine - Networking", func() { // Both NetworkID and NICIDs are empty - should fail validation }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) machineClass = &v1alpha1.MachineClass{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/provider/core_create_machine_storage_test.go b/pkg/provider/create_storage_test.go similarity index 78% rename from pkg/provider/core_create_machine_storage_test.go rename to pkg/provider/create_storage_test.go index 1f81dd0b..aae0dd3f 100644 --- a/pkg/provider/core_create_machine_storage_test.go +++ b/pkg/provider/create_storage_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -11,6 +7,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,7 +19,7 @@ var _ = Describe("CreateMachine", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.CreateMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -30,7 +28,7 @@ var _ = Describe("CreateMachine", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -50,7 +48,7 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -96,13 +94,13 @@ var _ = Describe("CreateMachine", func() { }, }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -131,13 +129,13 @@ var _ = Describe("CreateMachine", func() { Size: 50, }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -162,13 +160,13 @@ var _ = Describe("CreateMachine", func() { "660e8400-e29b-41d4-a716-446655440001", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -197,13 +195,13 @@ var _ = Describe("CreateMachine", func() { "550e8400-e29b-41d4-a716-446655440000", }, } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -220,10 +218,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send volumes when not specified", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", diff --git a/pkg/provider/core_create_machine_userdata_test.go b/pkg/provider/create_userdata_test.go similarity index 77% rename from pkg/provider/core_create_machine_userdata_test.go rename to pkg/provider/create_userdata_test.go index c0d90c7a..5176ad65 100644 --- a/pkg/provider/core_create_machine_userdata_test.go +++ b/pkg/provider/create_userdata_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -12,6 +8,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,7 +20,7 @@ var _ = Describe("CreateMachine", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.CreateMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -31,7 +29,7 @@ var _ = Describe("CreateMachine", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -51,7 +49,7 @@ var _ = Describe("CreateMachine", func() { ImageID: "12345678-1234-1234-1234-123456789abc", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -88,13 +86,13 @@ var _ = Describe("CreateMachine", func() { UserData: "#cloud-config\nruncmd:\n - echo 'Hello from ProviderSpec'", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -112,10 +110,10 @@ var _ = Describe("CreateMachine", func() { It("should pass userData from Secret to API when ProviderSpec.UserData is empty", func() { secret.Data["userData"] = []byte("#cloud-config\nruncmd:\n - echo 'Hello from Secret'") - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -137,14 +135,14 @@ var _ = Describe("CreateMachine", func() { UserData: "#cloud-config from ProviderSpec", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) req.MachineClass.ProviderSpec.Raw = providerSpecRaw secret.Data["userData"] = []byte("#cloud-config from Secret") - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -160,10 +158,10 @@ var _ = Describe("CreateMachine", func() { }) It("should not send userData when neither ProviderSpec nor Secret have it", func() { - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", @@ -180,10 +178,10 @@ var _ = Describe("CreateMachine", func() { It("should handle empty userData in Secret gracefully", func() { secret.Data["userData"] = []byte("") - var capturedReq *CreateServerRequest - mockClient.createServerFunc = func(_ context.Context, _, _ string, req *CreateServerRequest) (*Server, error) { + var capturedReq *client.CreateServerRequest + mockClient.CreateServerFunc = func(_ context.Context, _, _ string, req *client.CreateServerRequest) (*client.Server, error) { capturedReq = req - return &Server{ + return &client.Server{ ID: "test-server-id", Name: req.Name, Status: "CREATING", diff --git a/pkg/provider/delete.go b/pkg/provider/delete.go new file mode 100644 index 00000000..1379fbdc --- /dev/null +++ b/pkg/provider/delete.go @@ -0,0 +1,91 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "k8s.io/klog/v2" +) + +// DeleteMachine handles a machine deletion request by deleting the STACKIT server +// +// This method deletes the server identified by the ProviderID from STACKIT infrastructure. +// It is idempotent - if the server is already deleted (404), it returns success. +// +// Error codes: +// - InvalidArgument: Missing or invalid ProviderID +// - Internal: Failed to delete server or communicate with STACKIT API +func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineRequest) (*driver.DeleteMachineResponse, error) { + // Log messages to track delete request + klog.V(2).Infof("Machine deletion request has been received for %q", req.Machine.Name) + 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"]) + // 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)) + } + + var projectID, serverID string + var err error + if req.Machine.Spec.ProviderID != "" { + if !strings.HasPrefix(req.Machine.Spec.ProviderID, StackitProviderName) { + return nil, status.Error(codes.InvalidArgument, "providerID is not empty and does not start with stackit://") + } + + // Parse ProviderID to extract projectID and serverID + projectID, serverID, err = parseProviderID(req.Machine.Spec.ProviderID) + if err != nil { + klog.V(2).Infof("invalid ProviderID format: %v", err) + } + } + + if projectID == "" { + projectID = string(req.Secret.Data["project-id"]) + } + + providerSpec, err := decodeProviderSpec(req.MachineClass) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + if serverID == "" { + server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to find server by name: %v", err)) + } + + if server != nil { + serverID = server.ID + } + } + + if serverID == "" { + klog.V(2).Infof("Server is already deleted for machine %q", req.Machine.Name) + return &driver.DeleteMachineResponse{}, nil + } + + // Call STACKIT API to delete server + 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, client.ErrServerNotFound) { + klog.V(2).Infof("Server %q already deleted for machine %q (idempotent)", serverID, req.Machine.Name) + return &driver.DeleteMachineResponse{}, nil + } + // All other errors are internal errors + klog.Errorf("Failed to delete server for machine %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to delete server: %v", err)) + } + + klog.V(2).Infof("Successfully deleted server %q for machine %q", serverID, req.Machine.Name) + + return &driver.DeleteMachineResponse{}, nil +} diff --git a/pkg/provider/core_delete_machine_test.go b/pkg/provider/delete_test.go similarity index 86% rename from pkg/provider/core_delete_machine_test.go rename to pkg/provider/delete_test.go index a748d062..746f76fb 100644 --- a/pkg/provider/core_delete_machine_test.go +++ b/pkg/provider/delete_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -14,6 +10,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,7 +22,7 @@ var _ = Describe("DeleteMachine", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.DeleteMachineRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -33,7 +31,7 @@ var _ = Describe("DeleteMachine", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -53,7 +51,7 @@ var _ = Describe("DeleteMachine", func() { ImageID: "image-uuid-123", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -86,7 +84,7 @@ var _ = Describe("DeleteMachine", func() { Context("with valid inputs", func() { It("should successfully delete a machine", func() { - mockClient.deleteServerFunc = func(_ context.Context, _, _, _ string) error { + mockClient.DeleteServerFunc = func(_ context.Context, _, _, _ string) error { return nil } @@ -100,7 +98,7 @@ var _ = Describe("DeleteMachine", func() { var capturedProjectID string var capturedServerID string - mockClient.deleteServerFunc = func(_ context.Context, projectID, _, serverID string) error { + mockClient.DeleteServerFunc = func(_ context.Context, projectID, _, serverID string) error { capturedProjectID = projectID capturedServerID = serverID return nil @@ -137,8 +135,8 @@ var _ = Describe("DeleteMachine", func() { Context("when machine not found", func() { It("should return success if machine does not exist (idempotent)", func() { - mockClient.deleteServerFunc = func(_ context.Context, _, _, _ string) error { - return fmt.Errorf("%w: status 404", ErrServerNotFound) + mockClient.DeleteServerFunc = func(_ context.Context, _, _, _ string) error { + return fmt.Errorf("%w: status 404", client.ErrServerNotFound) } resp, err := provider.DeleteMachine(ctx, req) @@ -150,7 +148,7 @@ var _ = Describe("DeleteMachine", func() { Context("when STACKIT API fails", func() { It("should return error when API call fails", func() { - mockClient.deleteServerFunc = func(_ context.Context, _, _, _ string) error { + mockClient.DeleteServerFunc = func(_ context.Context, _, _, _ string) error { return fmt.Errorf("API connection failed") } diff --git a/pkg/provider/helpers.go b/pkg/provider/helpers.go index da6eb7a2..14127ff4 100644 --- a/pkg/provider/helpers.go +++ b/pkg/provider/helpers.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -55,61 +51,3 @@ func parseProviderID(providerID string) (projectID, serverID string, err error) return parts[0], parts[1], nil } - -// ========== SDK Conversion Helpers ========== - -// ptr returns a pointer to the given value -// This helper is needed because the STACKIT SDK uses pointers for optional fields -func ptr[T any](v T) *T { - return &v -} - -// convertLabelsToSDK converts map[string]string to *map[string]interface{} for SDK -// -//nolint:gocritic // SDK requires *map -func convertLabelsToSDK(labels map[string]string) *map[string]interface{} { - if labels == nil { - return nil - } - - result := make(map[string]interface{}, len(labels)) - for k, v := range labels { - result[k] = v - } - return &result -} - -// convertLabelsFromSDK converts *map[string]interface{} from SDK to map[string]string -// -//nolint:gocritic // SDK requires *map -func convertLabelsFromSDK(labels *map[string]interface{}) map[string]string { - if labels == nil { - return nil - } - - result := make(map[string]string, len(*labels)) - for k, v := range *labels { - if strVal, ok := v.(string); ok { - result[k] = strVal - } - } - return result -} - -// convertStringSliceToSDK converts []string to *[]string for SDK -func convertStringSliceToSDK(slice []string) *[]string { - if slice == nil { - return nil - } - return &slice -} - -// convertMetadataToSDK converts map[string]interface{} to *map[string]interface{} for SDK -// -//nolint:gocritic // SDK requires *map -func convertMetadataToSDK(metadata map[string]interface{}) *map[string]interface{} { - if metadata == nil { - return nil - } - return &metadata -} diff --git a/pkg/provider/helpers_test.go b/pkg/provider/helpers_test.go index 71cc35e9..70355525 100644 --- a/pkg/provider/helpers_test.go +++ b/pkg/provider/helpers_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( diff --git a/pkg/provider/list.go b/pkg/provider/list.go new file mode 100644 index 00000000..6367b8b2 --- /dev/null +++ b/pkg/provider/list.go @@ -0,0 +1,75 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" + "k8s.io/klog/v2" +) + +// 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 +// to detect and clean up orphan VMs that are not backed by Machine CRs. +// +// Returns: +// - MachineList: Map of ProviderID to MachineName for all servers matching the MachineClass +// +// Error codes: +// - Internal: Failed to list servers or communicate with STACKIT API +func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesRequest) (*driver.ListMachinesResponse, error) { + // Log messages to track start and end of request + klog.V(2).Infof("List machines request has been received for %q", req.MachineClass.Name) + 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"]) + + // 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 + labelSelector := map[string]string{ + StackitMachineClassLabel: req.MachineClass.Name, + } + servers, err := p.client.ListServers(ctx, projectID, providerSpec.Region, labelSelector) + 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)) + } + + // Filter servers by MachineClass label + // We use the "mcm.gardener.cloud/machineclass" label to identify which servers belong to this MachineClass + machineList := make(map[string]string) + for _, server := range servers { + // Generate ProviderID in format: stackit:/// + providerID := fmt.Sprintf("stackit://%s/%s", projectID, server.ID) + + // Get machine name from labels (fallback to server name if not found) + machineName := server.Name + if machineLabel, ok := server.Labels[StackitMachineLabel]; ok { + machineName = machineLabel + } + + machineList[providerID] = machineName + } + + klog.V(2).Infof("Found %d machines for MachineClass %q", len(machineList), req.MachineClass.Name) + + return &driver.ListMachinesResponse{ + MachineList: machineList, + }, nil +} diff --git a/pkg/provider/core_list_machines_test.go b/pkg/provider/list_test.go similarity index 80% rename from pkg/provider/core_list_machines_test.go rename to pkg/provider/list_test.go index 0e201372..cbaf18df 100644 --- a/pkg/provider/core_list_machines_test.go +++ b/pkg/provider/list_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -14,6 +10,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,7 +22,7 @@ var _ = Describe("ListMachines", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.ListMachinesRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -32,7 +30,7 @@ var _ = Describe("ListMachines", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -51,7 +49,7 @@ var _ = Describe("ListMachines", func() { ImageID: "image-uuid-123", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -72,10 +70,10 @@ 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) ([]*Server, error) { + mockClient.ListServersFunc = func(_ context.Context, _, _ string, selector map[string]string) ([]*client.Server, error) { Expect(selector["mcm.gardener.cloud/machineclass"]).To(Equal("test-machine-class")) - return []*Server{ + return []*client.Server{ { ID: "server-1", Name: "machine-1", @@ -106,8 +104,8 @@ var _ = Describe("ListMachines", func() { }) It("should return empty list when no servers match", func() { - mockClient.listServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*Server, error) { - return []*Server{}, nil + mockClient.ListServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*client.Server, error) { + return []*client.Server{}, nil } resp, err := provider.ListMachines(ctx, req) @@ -118,8 +116,8 @@ var _ = Describe("ListMachines", func() { }) It("should return empty list when no servers exist", func() { - mockClient.listServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*Server, error) { - return []*Server{}, nil + mockClient.ListServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*client.Server, error) { + return []*client.Server{}, nil } resp, err := provider.ListMachines(ctx, req) @@ -132,7 +130,7 @@ var _ = Describe("ListMachines", func() { Context("when STACKIT API fails", func() { It("should return Internal error on API failure", func() { - mockClient.listServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*Server, error) { + mockClient.ListServersFunc = func(_ context.Context, _, _ string, _ map[string]string) ([]*client.Server, error) { return nil, fmt.Errorf("API connection failed") } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 17e6ba29..992d9496 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -1,8 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -// Package provider contains the cloud provider specific implementations to manage machines package provider import ( @@ -10,6 +5,7 @@ import ( "sync" "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" + client2 "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/spi" "k8s.io/klog/v2" ) @@ -24,10 +20,10 @@ import ( // - Credential rotation requires pod restart (standard Kubernetes pattern) type Provider struct { SPI spi.SessionProviderInterface - client StackitClient // STACKIT API client (can be mocked for testing) - clientOnce sync.Once // Ensures client is initialized exactly once - clientErr error // Stores initialization error if any - capturedCredentials string // Service account key used for initialization (for defensive checks) + client client2.StackitClient // STACKIT API client (can be mocked for testing) + clientOnce sync.Once // Ensures client is initialized exactly once + clientErr error // Stores initialization error if any + capturedCredentials string // Service account key used for initialization (for defensive checks) } // NewProvider returns an empty provider object @@ -53,7 +49,7 @@ func (p *Provider) ensureClient(serviceAccountKey string) error { } p.clientOnce.Do(func() { - client, err := NewStackitClient(serviceAccountKey) + client, err := client2.NewStackitClient(serviceAccountKey) if err != nil { p.clientErr = fmt.Errorf("failed to initialize STACKIT client: %w", err) return diff --git a/pkg/provider/provider_suite_test.go b/pkg/provider/provider_suite_test.go index b4da7a44..25a87a60 100644 --- a/pkg/provider/provider_suite_test.go +++ b/pkg/provider/provider_suite_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( diff --git a/pkg/provider/status.go b/pkg/provider/status.go new file mode 100644 index 00000000..885ffe15 --- /dev/null +++ b/pkg/provider/status.go @@ -0,0 +1,84 @@ +package provider + +import ( + "context" + "errors" + "fmt" + + "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" + "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "k8s.io/klog/v2" +) + +// GetMachineStatus retrieves the current status of a STACKIT server +// +// This method queries STACKIT API to get the current state of the server identified +// by the Machine's ProviderID. If the ProviderID is empty (machine not created yet) +// or the server doesn't exist, it returns NotFound error. +// +// Returns: +// - ProviderID: The machine's ProviderID +// - NodeName: Name that the VM registered with in Kubernetes +// +// Error codes: +// - NotFound: Machine has no ProviderID yet, or server not found in STACKIT +// - InvalidArgument: Invalid ProviderID format +// - Internal: Failed to get server status or communicate with STACKIT API +func (p *Provider) GetMachineStatus(ctx context.Context, req *driver.GetMachineStatusRequest) (*driver.GetMachineStatusResponse, error) { + // Log messages to track start and end of request + klog.V(2).Infof("Get request has been received for %q", req.Machine.Name) + defer klog.V(2).Infof("Machine get request has been processed successfully for %q", req.Machine.Name) + + // When ProviderID is empty, the machine doesn't exist yet + // Return NotFound so MCM knows to call CreateMachine + if req.Machine.Spec.ProviderID == "" { + klog.V(2).Infof("Machine %q has no ProviderID, returning NotFound", req.Machine.Name) + return nil, status.Error(codes.NotFound, "machine does not have a ProviderID yet") + } + + // Extract credentials from Secret + serviceAccountKey := string(req.Secret.Data["serviceaccount.json"]) + + // 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)) + } + + // Parse ProviderID to extract projectID and serverID + // Expected format: stackit:/// + projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID) + if projectID == "" { + projectID = string(req.Secret.Data["project-id"]) + } + if err != nil { + 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, providerSpec.Region, serverID) + if err != nil { + // Check if server was not found (404) + if errors.Is(err, client.ErrServerNotFound) { + klog.V(2).Infof("Server %q not found for machine %q", serverID, req.Machine.Name) + return nil, status.Error(codes.NotFound, fmt.Sprintf("server %q not found", serverID)) + } + // All other errors are internal errors + klog.Errorf("Failed to get server status for machine %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get server status: %v", err)) + } + + klog.V(2).Infof("Retrieved server status for machine %q: status=%s", req.Machine.Name, server.Status) + + return &driver.GetMachineStatusResponse{ + ProviderID: req.Machine.Spec.ProviderID, + NodeName: req.Machine.Name, + }, nil +} diff --git a/pkg/provider/core_get_machine_status_test.go b/pkg/provider/status_test.go similarity index 85% rename from pkg/provider/core_get_machine_status_test.go rename to pkg/provider/status_test.go index 4470d700..e52440af 100644 --- a/pkg/provider/core_get_machine_status_test.go +++ b/pkg/provider/status_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package provider import ( @@ -14,6 +10,8 @@ import ( "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client" + "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client/mock" api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,7 +22,7 @@ var _ = Describe("GetMachineStatus", func() { var ( ctx context.Context provider *Provider - mockClient *mockStackitClient + mockClient *mock.StackitClient req *driver.GetMachineStatusRequest secret *corev1.Secret machineClass *v1alpha1.MachineClass @@ -33,7 +31,7 @@ var _ = Describe("GetMachineStatus", func() { BeforeEach(func() { ctx = context.Background() - mockClient = &mockStackitClient{} + mockClient = &mock.StackitClient{} provider = &Provider{ client: mockClient, } @@ -53,7 +51,7 @@ var _ = Describe("GetMachineStatus", func() { ImageID: "image-uuid-123", Region: "eu01", } - providerSpecRaw, _ := encodeProviderSpec(providerSpec) + providerSpecRaw, _ := mock.EncodeProviderSpec(providerSpec) // Create MachineClass machineClass = &v1alpha1.MachineClass{ @@ -86,8 +84,8 @@ var _ = Describe("GetMachineStatus", func() { Context("with valid inputs", func() { It("should successfully get machine status when server exists", func() { - mockClient.getServerFunc = func(_ context.Context, _, _, serverID string) (*Server, error) { - return &Server{ + mockClient.GetServerFunc = func(_ context.Context, _, _, serverID string) (*client.Server, error) { + return &client.Server{ ID: serverID, Name: "test-machine", Status: "ACTIVE", @@ -106,10 +104,10 @@ var _ = Describe("GetMachineStatus", func() { var capturedProjectID string var capturedServerID string - mockClient.getServerFunc = func(_ context.Context, projectID, _, serverID string) (*Server, error) { + mockClient.GetServerFunc = func(_ context.Context, projectID, _, serverID string) (*client.Server, error) { capturedProjectID = projectID capturedServerID = serverID - return &Server{ + return &client.Server{ ID: serverID, Name: "test-machine", Status: "ACTIVE", @@ -161,8 +159,8 @@ var _ = Describe("GetMachineStatus", func() { Context("when server does not exist", func() { It("should return NotFound when server is not found", func() { - mockClient.getServerFunc = func(_ context.Context, _, _, _ string) (*Server, error) { - return nil, fmt.Errorf("%w: status 404", ErrServerNotFound) + mockClient.GetServerFunc = func(_ context.Context, _, _, _ string) (*client.Server, error) { + return nil, fmt.Errorf("%w: status 404", client.ErrServerNotFound) } _, err := provider.GetMachineStatus(ctx, req) @@ -176,7 +174,7 @@ var _ = Describe("GetMachineStatus", func() { Context("when STACKIT API fails", func() { It("should return Internal error on API failure", func() { - mockClient.getServerFunc = func(_ context.Context, _, _, _ string) (*Server, error) { + mockClient.GetServerFunc = func(_ context.Context, _, _, _ string) (*client.Server, error) { return nil, fmt.Errorf("API connection failed") } diff --git a/pkg/spi/spi.go b/pkg/spi/spi.go index d50330de..c8eacaff 100644 --- a/pkg/spi/spi.go +++ b/pkg/spi/spi.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package spi // SessionProviderInterface provides an interface to deal with cloud provider session diff --git a/scripts/Dockerfile_build b/scripts/Dockerfile_build index 0c8698bd..e9dbd040 100644 --- a/scripts/Dockerfile_build +++ b/scripts/Dockerfile_build @@ -1,6 +1,3 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -# SPDX-License-Identifier: Apache-2.0 -# # Dockerfile for building binary inside Docker (exports to host) FROM golang:1.25.5 AS builder diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh index 751c2af8..c4f66982 100755 --- a/scripts/build-docker.sh +++ b/scripts/build-docker.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -# SPDX-License-Identifier: Apache-2.0 -# # Build binary inside Docker container (no local Go required) set -euo pipefail diff --git a/scripts/build.sh b/scripts/build.sh index 1a39eea9..e33f33c8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -# SPDX-License-Identifier: Apache-2.0 set -euo pipefail diff --git a/test/e2e/common.go b/test/e2e/common.go index e289efc8..7ff60ff6 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_affinity_test.go b/test/e2e/e2e_affinity_test.go index 5977be63..ff9b0559 100644 --- a/test/e2e/e2e_affinity_test.go +++ b/test/e2e/e2e_affinity_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_agent_test.go b/test/e2e/e2e_agent_test.go index 0437adef..ae544c06 100644 --- a/test/e2e/e2e_agent_test.go +++ b/test/e2e/e2e_agent_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_az_test.go b/test/e2e/e2e_az_test.go index 2f260c5c..1eb7aeb9 100644 --- a/test/e2e/e2e_az_test.go +++ b/test/e2e/e2e_az_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_basic_test.go b/test/e2e/e2e_basic_test.go index 5ab77f41..19363742 100644 --- a/test/e2e/e2e_basic_test.go +++ b/test/e2e/e2e_basic_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_keypair_test.go b/test/e2e/e2e_keypair_test.go index 46ac7193..1e29f907 100644 --- a/test/e2e/e2e_keypair_test.go +++ b/test/e2e/e2e_keypair_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_labels_test.go b/test/e2e/e2e_labels_test.go index 4b686a28..a1d25e36 100644 --- a/test/e2e/e2e_labels_test.go +++ b/test/e2e/e2e_labels_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_lifecycle_test.go b/test/e2e/e2e_lifecycle_test.go index 01981428..e3cab91c 100644 --- a/test/e2e/e2e_lifecycle_test.go +++ b/test/e2e/e2e_lifecycle_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_metadata_test.go b/test/e2e/e2e_metadata_test.go index 9cf2dd11..1fe6623a 100644 --- a/test/e2e/e2e_metadata_test.go +++ b/test/e2e/e2e_metadata_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_networking_test.go b/test/e2e/e2e_networking_test.go index 24d5cba2..b2c6d782 100644 --- a/test/e2e/e2e_networking_test.go +++ b/test/e2e/e2e_networking_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_service_accounts_test.go b/test/e2e/e2e_service_accounts_test.go index a9a5791a..e332b4c8 100644 --- a/test/e2e/e2e_service_accounts_test.go +++ b/test/e2e/e2e_service_accounts_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index cfa50009..ed587d6b 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_userdata_test.go b/test/e2e/e2e_userdata_test.go index 1722dbab..1f475411 100644 --- a/test/e2e/e2e_userdata_test.go +++ b/test/e2e/e2e_userdata_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/e2e/e2e_volumes_test.go b/test/e2e/e2e_volumes_test.go index 9dcc30a9..430228aa 100644 --- a/test/e2e/e2e_volumes_test.go +++ b/test/e2e/e2e_volumes_test.go @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - package e2e import ( diff --git a/test/utils/utils.go b/test/utils/utils.go index be705a56..d6c8b573 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -1,19 +1,3 @@ -/* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - package utils import (