Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions internal/provider/instances_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ type InstancesV2 struct {
// InstanceExists checks whether the provided Kubernetes node exists as instance
// in Oxide.
func (i *InstancesV2) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
instanceID, err := InstanceIDFromProviderID(node.Spec.ProviderID)
if err != nil {
return false, fmt.Errorf("failed retrieving instance id from provider id: %w", err)
}

if _, err := i.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
Expand All @@ -50,7 +53,10 @@ func (i *InstancesV2) InstanceMetadata(ctx context.Context, node *v1.Node) (*clo
)

if node.Spec.ProviderID != "" {
instanceID = strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
instanceID, err = InstanceIDFromProviderID(node.Spec.ProviderID)
if err != nil {
return nil, fmt.Errorf("failed retrieving instance id from provider id: %w", err)
}

instance, err = i.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
Expand Down Expand Up @@ -109,15 +115,18 @@ func (i *InstancesV2) InstanceMetadata(ctx context.Context, node *v1.Node) (*clo
}

return &cloudprovider.InstanceMetadata{
ProviderID: fmt.Sprintf("oxide://%s", instanceID),
ProviderID: NewProviderID(instanceID),
InstanceType: fmt.Sprintf("%v-%v", instance.Ncpus, (instance.Memory / (1024 * 1024 * 1024))),
NodeAddresses: nodeAddresses,
}, nil
}

// InstanceShutdown checks whether the provided node is shut down in Oxide.
func (i *InstancesV2) InstanceShutdown(ctx context.Context, node *v1.Node) (bool, error) {
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
instanceID, err := InstanceIDFromProviderID(node.Spec.ProviderID)
if err != nil {
return false, fmt.Errorf("failed retrieving instance id from provider id: %w", err)
}

instance, err := i.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
Expand Down
28 changes: 28 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package provider

import (
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/google/uuid"
"github.com/oxidecomputer/oxide.go/oxide"
"k8s.io/client-go/kubernetes"
cloudprovider "k8s.io/cloud-provider"
Expand Down Expand Up @@ -116,3 +120,27 @@ func (o *Oxide) Routes() (cloudprovider.Routes, bool) {
func (o *Oxide) Zones() (cloudprovider.Zones, bool) {
return nil, false
}

// InstanceIDFromProviderID extracts the Oxide instance ID from a provider ID.
func InstanceIDFromProviderID(providerID string) (string, error) {
if providerID == "" {
return "", errors.New("provider id is empty")
}

if !strings.HasPrefix(providerID, "oxide://") {
return "", errors.New("provider id does not have 'oxide://' prefix")
}

instanceID := strings.TrimPrefix(providerID, "oxide://")

if _, err := uuid.Parse(instanceID); err != nil {
return "", fmt.Errorf("provider id contains invalid uuid: %w", err)
}

return instanceID, nil
}

// NewProviderID formats an Oxide instance ID as a provider ID.
func NewProviderID(instanceID string) string {
return fmt.Sprintf("oxide://%s", instanceID)
}
125 changes: 125 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package provider

import (
"strings"
"testing"
)

func TestInstanceIDFromProviderID(t *testing.T) {
t.Run("Success", func(t *testing.T) {
tt := []struct {
name string
providerID string
expected string
}{
{
name: "valid provider ID with UUID",
providerID: "oxide://12345678-1234-1234-1234-123456789abc",
expected: "12345678-1234-1234-1234-123456789abc",
},
{
name: "valid provider ID with uppercase UUID",
providerID: "oxide://12345678-1234-1234-1234-123456789ABC",
expected: "12345678-1234-1234-1234-123456789ABC",
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
result, err := InstanceIDFromProviderID(tc.providerID)
if err != nil {
t.Errorf("TestInstanceIDFromProviderID(%s) returned non-nil error %v, want nil error", tc.providerID, err)
}

if result != tc.expected {
t.Errorf("TestInstanceIDFromProviderID(%s) returned %s, want %s", tc.providerID, result, tc.expected)
}
})
}
})

t.Run("Error", func(t *testing.T) {
tt := []struct {
name string
providerID string
errorMsg string
}{
{
name: "empty provider ID",
providerID: "",
errorMsg: "provider id is empty",
},
{
name: "provider ID without oxide:// prefix",
providerID: "12345678-1234-1234-1234-123456789abc",
errorMsg: "provider id does not have 'oxide://' prefix",
},
{
name: "provider ID with invalid UUID",
providerID: "oxide://not-a-valid-uuid",
errorMsg: "provider id contains invalid uuid",
},
{
name: "provider ID with empty UUID",
providerID: "oxide://",
errorMsg: "provider id contains invalid uuid",
},
{
name: "provider ID with partial UUID",
providerID: "oxide://12345678-1234",
errorMsg: "provider id contains invalid uuid",
},
{
name: "provider ID with wrong prefix",
providerID: "aws://12345678-1234-1234-1234-123456789abc",
errorMsg: "provider id does not have 'oxide://' prefix",
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
_, err := InstanceIDFromProviderID(tc.providerID)
if err == nil {
t.Errorf("TestInstanceIDFromProviderID(%s) returned nil error, want non-nil error", tc.providerID)
}

if !strings.Contains(err.Error(), tc.errorMsg) {
t.Errorf("TestInstanceIDFromProviderID(%s) returned error %v, want %s", tc.providerID, err.Error(), tc.errorMsg)
}
})
}
})
}

func TestNewProviderID(t *testing.T) {
tests := []struct {
name string
instanceID string
expected string
}{
{
name: "valid instance ID",
instanceID: "12345678-1234-1234-1234-123456789abc",
expected: "oxide://12345678-1234-1234-1234-123456789abc",
},
{
name: "empty instance ID",
instanceID: "",
expected: "oxide://",
},
{
name: "instance ID with uppercase UUID",
instanceID: "12345678-1234-1234-1234-123456789ABC",
expected: "oxide://12345678-1234-1234-1234-123456789ABC",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewProviderID(tt.instanceID)
if result != tt.expected {
t.Errorf("NewProviderID(%s) = %s, want %s", tt.instanceID, result, tt.expected)
}
})
}
}