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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/oxidecomputer/oxide-cloud-controller-manager

go 1.25.0
go 1.25.3

require (
github.com/oxidecomputer/oxide.go v0.6.0
Expand Down
130 changes: 130 additions & 0 deletions internal/provider/instances_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package provider

import (
"context"
"fmt"
"strings"

"github.com/oxidecomputer/oxide.go/oxide"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
cloudprovider "k8s.io/cloud-provider"
)

var _ cloudprovider.InstancesV2 = (*InstancesV2)(nil)

// InstancesV2 implements [cloudprovider.InstancesV2] to provide Oxide specific
// instance functionality.
type InstancesV2 struct {
client *oxide.Client
project string

k8sClient kubernetes.Interface
}

// InstanceExists checks whether the provided Kubernetes node exists as instance
// in Oxide.
func (c *InstancesV2) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")

if _, err := c.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
}); err != nil {
if strings.Contains(err.Error(), "NotFound") {
return false, nil
}

return false, fmt.Errorf("failed viewing oxide instance %s: %v", instanceID, err)
}

return true, nil
}

// InstanceMetadata populates the metadata for the provided node, notably
// setting its provider ID.
func (c *InstancesV2) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
var (
err error
instance *oxide.Instance
instanceID string
)

if node.Spec.ProviderID != "" {
instanceID = strings.TrimPrefix(node.Spec.ProviderID, "oxide://")

instance, err = c.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
})
if err != nil {
return nil, fmt.Errorf("failed viewing oxide instance by id: %v", err)
}
} else {
instance, err = c.client.InstanceView(ctx, oxide.InstanceViewParams{
Project: oxide.NameOrId(c.project),
Instance: oxide.NameOrId(node.GetName()),
})
if err != nil {
return nil, fmt.Errorf("failed viewing oxide instance by name: %v", err)
}

instanceID = instance.Id
}

nics, err := c.client.InstanceNetworkInterfaceList(ctx, oxide.InstanceNetworkInterfaceListParams{
Instance: oxide.NameOrId(instanceID),
})
if err != nil {
return nil, fmt.Errorf("failed listing instance network interfaces: %v", err)
}

externalIPs, err := c.client.InstanceExternalIpList(ctx, oxide.InstanceExternalIpListParams{
Instance: oxide.NameOrId(instanceID),
})
if err != nil {
return nil, fmt.Errorf("failed listing instance external ips: %v", err)
}

nodeAddresses := make([]v1.NodeAddress, 0)
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
Type: v1.NodeHostName,
Address: instance.Hostname,
})

for _, nic := range nics.Items {
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
Type: v1.NodeInternalIP,
Address: nic.Ip,
})
}

for _, externalIP := range externalIPs.Items {
if externalIP.Kind == "snat" {
continue
}

nodeAddresses = append(nodeAddresses, v1.NodeAddress{
Type: v1.NodeExternalIP,
Address: externalIP.Ip,
})
}

return &cloudprovider.InstanceMetadata{
ProviderID: fmt.Sprintf("oxide://%s", 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 (c *InstancesV2) InstanceShutdown(ctx context.Context, node *v1.Node) (bool, error) {
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")

instance, err := c.client.InstanceView(ctx, oxide.InstanceViewParams{
Instance: oxide.NameOrId(instanceID),
})
if err != nil {
return false, fmt.Errorf("failed viewing oxide instance %s: %v", instanceID, err)
}

return instance.RunState == oxide.InstanceStateStopped, nil
}
118 changes: 118 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package provider

import (
"io"
"os"

"github.com/oxidecomputer/oxide.go/oxide"
"k8s.io/client-go/kubernetes"
cloudprovider "k8s.io/cloud-provider"
"k8s.io/klog/v2"
)

// init registers the Oxide cloud provider as a valid external cloud provider
// for Kubernetes.
func init() {
cloudprovider.RegisterCloudProvider(
Name,
func(config io.Reader) (cloudprovider.Interface, error) {
return &Oxide{}, nil
},
)
}

// Name is the name of this cloud provider.
const Name = "oxide"

var _ cloudprovider.Interface = (*Oxide)(nil)

// Oxide is the Oxide cloud provider. It implements [cloudprovider.Interface] to
// provide Oxide specific functionality.
type Oxide struct {
client *oxide.Client
project string

k8sClient kubernetes.Interface
}

// Initialize creates the Oxide and Kubernetes clients and spawns any additional
// controllers, if necessary.
func (c *Oxide) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
kubernetesClient, err := clientBuilder.Client(Name)
if err != nil {
klog.Fatalf("failed to create kubernetes client: %v", err)
return
}
c.k8sClient = kubernetesClient

oxideClient, err := oxide.NewClient(nil)
if err != nil {
klog.Fatalf("failed to create oxide client: %v", err)
return
}
c.client = oxideClient

c.project = os.Getenv("OXIDE_PROJECT")

klog.InfoS("initialized cloud provider", "type", "oxide")
}

// ProviderName returns the name of this cloud provider.
func (c *Oxide) ProviderName() string {
return Name
}

// HasClusterID is purposefully unimplemented. A cluster ID is used to uniquely
// identify resources for a specific Kubernetes cluster when multiple Kubernetes
// clusters can conflict with each other when using shared resources (e.g.,
// VPC, load balancer). Usually, such resources are tagged or labeled with the
// cluster ID but Oxide does not have resource tags or labels. Additionally,
// it's expected that a Kubernetes cluster on Oxide is deployed in its own VPC
// and does not share resources with other Kubernetes clusters. This may become
// supported in the future when Oxide has resource tags or labels.
func (c *Oxide) HasClusterID() bool {
return false
}

// Clusters is purposefully unimplemented. This is meant for a single Cloud
// Controller Manager to manage multiple Kubernetes clusters but the modern
// idiom is to run a single Cloud Controller Manager per Kubernetes cluster,
// making this irrelevant.
func (c *Oxide) Clusters() (cloudprovider.Clusters, bool) {
return nil, false
}

// Instances is purposefully unimplemented. Use [Oxide.InstancesV2].
func (c *Oxide) Instances() (cloudprovider.Instances, bool) {
return nil, false
}

// InstancesV2 returns an implementation of [cloudprovider.InstancesV2]
// that provides functionality to initialize Kubernetes nodes, provide their
// metadata, and determine whether they exists to facilitate cleanup.
func (c *Oxide) InstancesV2() (cloudprovider.InstancesV2, bool) {
return &InstancesV2{
client: c.client,
project: c.project,
k8sClient: c.k8sClient,
}, true
}

// LoadBalancer is currently unimplemented. This may be implemented in the
// future.
func (c *Oxide) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
return nil, false
}

// Routes is purposefully unimplemented. It is expected that the Kubernetes
// cluster uses a third-party CNI instead of this controller. This may be
// implemented in the future.
func (c *Oxide) Routes() (cloudprovider.Routes, bool) {
return nil, false
}

// Zones is purposefully unimplemented. Zone and region information is retrieved
// from [InstancesV2.InstanceMetadata] instead.
func (c *Oxide) Zones() (cloudprovider.Zones, bool) {
return nil, false
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
_ "k8s.io/component-base/metrics/prometheus/version"
"k8s.io/klog/v2"

_ "github.com/oxidecomputer/oxide-cloud-controller-manager/pkg/cloudprovider/oxide"
_ "github.com/oxidecomputer/oxide-cloud-controller-manager/internal/provider"
)

func main() {
Expand Down
Loading