diff --git a/pkg/client/sdk.go b/pkg/client/sdk.go index dea59f27..239e73f8 100644 --- a/pkg/client/sdk.go +++ b/pkg/client/sdk.go @@ -38,6 +38,7 @@ func NewStackitClient(serviceAccountKey string) (*SdkStackitClient, error) { var ( // ErrServerNotFound indicates the server was not found (404) ErrServerNotFound = errors.New("server not found") + ErrNicNotFound = errors.New("nic not found") ) // createIAASClient creates a new STACKIT SDK IAAS API client @@ -296,6 +297,36 @@ func (c *SdkStackitClient) GetNICsForServer(ctx context.Context, projectID, regi return nics, nil } +func (c *SdkStackitClient) ListNICs(ctx context.Context, projectID, region, networkID string) ([]*NIC, error) { + res, err := c.iaasClient.DefaultAPI.ListNics(ctx, projectID, region, networkID).Execute() + if err != nil { + return nil, fmt.Errorf("SDK ListServerNICs failed: %w", err) + } + + if res.Items == nil { + return []*NIC{}, nil + } + + nics := make([]*NIC, 0) + for _, nic := range res.Items { + nics = append(nics, convertSDKNICtoNIC(&nic)) + } + + return nics, nil +} + +func (c *SdkStackitClient) DeleteNIC(ctx context.Context, projectID, region, networkID, nicID string) error { + err := c.iaasClient.DefaultAPI.DeleteNic(ctx, projectID, region, networkID, nicID).Execute() + if err != nil { + // Check if error is 404 Not Found - this is OK (idempotent) + if isNotFoundError(err) { + return fmt.Errorf("%w: %v", ErrNicNotFound, err) + } + return fmt.Errorf("SDK DeleteNic failed: %w", err) + } + return nil +} + func (c *SdkStackitClient) UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*NIC, error) { addresses := make([]iaas.AllowedAddressesInner, len(allowedAddresses)) @@ -337,6 +368,7 @@ func convertSDKNICtoNIC(nic *iaas.NIC) *NIC { AllowedAddresses: addresses, IPv4: nic.GetIpv4(), IPv6: nic.GetIpv6(), + Name: getStringValue(nic.Name), } } @@ -361,3 +393,10 @@ func isNotFoundError(err error) bool { } return false } + +func getStringValue(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/pkg/client/stackit.go b/pkg/client/stackit.go index 41b4810a..7201f355 100644 --- a/pkg/client/stackit.go +++ b/pkg/client/stackit.go @@ -25,6 +25,10 @@ type StackitClient interface { ListServers(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*Server, error) // GetNICsForServer retrieves a network interfaces for a given server GetNICsForServer(ctx context.Context, projectID, region, serverID string) ([]*NIC, error) + // ListNics list all nics for a network + ListNICs(ctx context.Context, projectID, region, networkID string) ([]*NIC, error) + // DeleteNIC delete a given nic by ID + DeleteNIC(ctx context.Context, projectID, region, networkID, nicID string) error // UpdateNIC updates a network interface UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*NIC, error) } @@ -95,4 +99,5 @@ type NIC struct { AllowedAddresses []string `json:"allowedAddresses,omitempty"` IPv4 string `json:"ipv4,omitempty"` IPv6 string `json:"ipv6,omitempty"` + Name string `json:"name"` } diff --git a/pkg/provider/core.go b/pkg/provider/core.go index 1cfacc0c..237047ae 100644 --- a/pkg/provider/core.go +++ b/pkg/provider/core.go @@ -13,6 +13,8 @@ const ( StackitProviderName = "stackit" StackitMachineLabel = "kubernetes.io/machine" StackitMachineClassLabel = "kubernetes.io/machineclass" + + migratedMachineAnnotation = "stackit.cloud/migrated-machine" ) // GetVolumeIDs extracts volume IDs from PersistentVolume specs diff --git a/pkg/provider/create.go b/pkg/provider/create.go index 930600b5..a6eaad4c 100644 --- a/pkg/provider/create.go +++ b/pkg/provider/create.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "slices" + "strconv" "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" "github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes" @@ -47,6 +48,10 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR return nil, status.Error(codes.InvalidArgument, err.Error()) } + if m, _ := strconv.ParseBool(req.Machine.Annotations[migratedMachineAnnotation]); m { + return nil, status.Error(codes.AlreadyExists, fmt.Errorf("create for migrated machine %s will not work", req.Machine.Name).Error()) + } + // Decode ProviderSpec from MachineClass providerSpec, err := decodeProviderSpec(req.MachineClass) if err != nil { @@ -68,12 +73,25 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR } // check if server already exists - server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) + servers, err := p.getServersByName(ctx, projectID, providerSpec.Region, map[string]string{ + StackitMachineLabel: 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 len(servers) > 1 { + klog.Errorf("Multiple servers already exists for this machine %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.AlreadyExists, fmt.Sprintf("failed to fetch server: %v", err)) + } + + var server *client.Server + + if len(servers) == 1 { + server = servers[0] + } + if server == nil { // Call STACKIT API to create server server, err = p.client.CreateServer(ctx, projectID, providerSpec.Region, p.createServerRequest(req, providerSpec)) @@ -233,26 +251,18 @@ func nicAddresses(nics []*client.NIC) []corev1.NodeAddress { return addresses } -func (p *Provider) getServerByName(ctx context.Context, projectID, region, serverName string) (*client.Server, error) { +func (p *Provider) getServersByName(ctx context.Context, projectID, region string, selector map[string]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) + servers, err := p.client.ListServers(ctx, projectID, region, selector) if err != nil { - return nil, fmt.Errorf("SDK ListServers with labelSelector: %v failed: %w", labelSelector, err) + return nil, fmt.Errorf("SDK ListServers with labelSelector: %v failed: %w", selector, 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 + if len(servers) == 0 { + return nil, nil } - // no servers found len == 0 - return nil, nil + return servers, nil } func (p *Provider) patchNetworkInterfaces(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) ([]*client.NIC, error) { diff --git a/pkg/provider/delete.go b/pkg/provider/delete.go index ff84e93c..15888d50 100644 --- a/pkg/provider/delete.go +++ b/pkg/provider/delete.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "github.com/gardener/machine-controller-manager/pkg/util/provider/driver" @@ -35,9 +36,11 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR return nil, status.Error(codes.Unauthenticated, fmt.Sprintf("failed to initialize STACKIT client: %v", err)) } - var projectID, serverID string + var projectID string + var serverIDs []string var err error if req.Machine.Spec.ProviderID != "" { + var serverID string if !strings.HasPrefix(req.Machine.Spec.ProviderID, StackitProviderName) { return nil, status.Error(codes.InvalidArgument, "providerID is not empty and does not start with stackit://") } @@ -47,6 +50,7 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR if err != nil { klog.V(2).Infof("invalid ProviderID format: %v", err) } + serverIDs = append(serverIDs, serverID) } if projectID == "" { @@ -59,39 +63,67 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR return nil, status.Error(codes.Internal, err.Error()) } - if serverID == "" { - server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name) + if len(serverIDs) == 0 { + selector := map[string]string{ + StackitMachineLabel: req.Machine.Name, + } + + if m, _ := strconv.ParseBool(req.Machine.Annotations[migratedMachineAnnotation]); m { + selector = nil + } + + servers, err := p.getServersByName(ctx, projectID, providerSpec.Region, selector) 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 + for _, server := range servers { + if server.Name != req.Machine.Name { + continue + } + serverIDs = append(serverIDs, server.ID) } } - if serverID == "" { - klog.V(2).Infof("Server is already deleted for machine %q", req.Machine.Name) - return &driver.DeleteMachineResponse{}, nil + for _, id := range serverIDs { + // Call STACKIT API to delete server + err = p.client.DeleteServer(ctx, projectID, providerSpec.Region, id) + 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)", id, 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)) + } } - // 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 + if m, _ := strconv.ParseBool(req.Machine.Annotations[migratedMachineAnnotation]); m { + nics, err := p.client.ListNICs(ctx, projectID, providerSpec.Region, providerSpec.Networking.NetworkID) + if err != nil { + return nil, err + } + for _, nic := range nics { + if nic.Name != req.Machine.Name { + continue + } + err = p.client.DeleteNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID) + if err != nil { + // Check if server was not found (404) - this is OK for idempotency + if errors.Is(err, client.ErrNicNotFound) { + klog.V(2).Infof("Nic %q already deleted for machine %q (idempotent)", nic.ID, req.Machine.Name) + return &driver.DeleteMachineResponse{}, nil + } + // All other errors are internal errors + klog.Errorf("Failed to delete nic for machine %q: %v", req.Machine.Name, err) + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to delete nic: %v", err)) + } } - // 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)) - } - if err := p.WaitUntilServerDeleted(ctx, projectID, providerSpec.Region, serverID); err != nil { - klog.Errorf("Failed waiting for server %q to be deleted for machine %q: %v", serverID, req.Machine.Name, err) - return nil, status.Error(codes.DeadlineExceeded, fmt.Sprintf("failed waiting for server to be deleted: %v", err)) } + klog.V(2).Infof("Successfully deleted server for machine %q", req.Machine.Name) return &driver.DeleteMachineResponse{}, nil }