Skip to content

Commit db26abb

Browse files
authored
chore: refactor package layout (#135)
Updated the package layout to better represent what the code does.
1 parent e4e341c commit db26abb

File tree

5 files changed

+250
-238
lines changed

5 files changed

+250
-238
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/oxidecomputer/oxide-cloud-controller-manager
22

3-
go 1.25.0
3+
go 1.25.3
44

55
require (
66
github.com/oxidecomputer/oxide.go v0.6.0

internal/provider/instances_v2.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/oxidecomputer/oxide.go/oxide"
9+
v1 "k8s.io/api/core/v1"
10+
"k8s.io/client-go/kubernetes"
11+
cloudprovider "k8s.io/cloud-provider"
12+
)
13+
14+
var _ cloudprovider.InstancesV2 = (*InstancesV2)(nil)
15+
16+
// InstancesV2 implements [cloudprovider.InstancesV2] to provide Oxide specific
17+
// instance functionality.
18+
type InstancesV2 struct {
19+
client *oxide.Client
20+
project string
21+
22+
k8sClient kubernetes.Interface
23+
}
24+
25+
// InstanceExists checks whether the provided Kubernetes node exists as instance
26+
// in Oxide.
27+
func (c *InstancesV2) InstanceExists(ctx context.Context, node *v1.Node) (bool, error) {
28+
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
29+
30+
if _, err := c.client.InstanceView(ctx, oxide.InstanceViewParams{
31+
Instance: oxide.NameOrId(instanceID),
32+
}); err != nil {
33+
if strings.Contains(err.Error(), "NotFound") {
34+
return false, nil
35+
}
36+
37+
return false, fmt.Errorf("failed viewing oxide instance %s: %v", instanceID, err)
38+
}
39+
40+
return true, nil
41+
}
42+
43+
// InstanceMetadata populates the metadata for the provided node, notably
44+
// setting its provider ID.
45+
func (c *InstancesV2) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloudprovider.InstanceMetadata, error) {
46+
var (
47+
err error
48+
instance *oxide.Instance
49+
instanceID string
50+
)
51+
52+
if node.Spec.ProviderID != "" {
53+
instanceID = strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
54+
55+
instance, err = c.client.InstanceView(ctx, oxide.InstanceViewParams{
56+
Instance: oxide.NameOrId(instanceID),
57+
})
58+
if err != nil {
59+
return nil, fmt.Errorf("failed viewing oxide instance by id: %v", err)
60+
}
61+
} else {
62+
instance, err = c.client.InstanceView(ctx, oxide.InstanceViewParams{
63+
Project: oxide.NameOrId(c.project),
64+
Instance: oxide.NameOrId(node.GetName()),
65+
})
66+
if err != nil {
67+
return nil, fmt.Errorf("failed viewing oxide instance by name: %v", err)
68+
}
69+
70+
instanceID = instance.Id
71+
}
72+
73+
nics, err := c.client.InstanceNetworkInterfaceList(ctx, oxide.InstanceNetworkInterfaceListParams{
74+
Instance: oxide.NameOrId(instanceID),
75+
})
76+
if err != nil {
77+
return nil, fmt.Errorf("failed listing instance network interfaces: %v", err)
78+
}
79+
80+
externalIPs, err := c.client.InstanceExternalIpList(ctx, oxide.InstanceExternalIpListParams{
81+
Instance: oxide.NameOrId(instanceID),
82+
})
83+
if err != nil {
84+
return nil, fmt.Errorf("failed listing instance external ips: %v", err)
85+
}
86+
87+
nodeAddresses := make([]v1.NodeAddress, 0)
88+
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
89+
Type: v1.NodeHostName,
90+
Address: instance.Hostname,
91+
})
92+
93+
for _, nic := range nics.Items {
94+
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
95+
Type: v1.NodeInternalIP,
96+
Address: nic.Ip,
97+
})
98+
}
99+
100+
for _, externalIP := range externalIPs.Items {
101+
if externalIP.Kind == "snat" {
102+
continue
103+
}
104+
105+
nodeAddresses = append(nodeAddresses, v1.NodeAddress{
106+
Type: v1.NodeExternalIP,
107+
Address: externalIP.Ip,
108+
})
109+
}
110+
111+
return &cloudprovider.InstanceMetadata{
112+
ProviderID: fmt.Sprintf("oxide://%s", instanceID),
113+
InstanceType: fmt.Sprintf("%v-%v", instance.Ncpus, (instance.Memory / (1024 * 1024 * 1024))),
114+
NodeAddresses: nodeAddresses,
115+
}, nil
116+
}
117+
118+
// InstanceShutdown checks whether the provided node is shut down in Oxide.
119+
func (c *InstancesV2) InstanceShutdown(ctx context.Context, node *v1.Node) (bool, error) {
120+
instanceID := strings.TrimPrefix(node.Spec.ProviderID, "oxide://")
121+
122+
instance, err := c.client.InstanceView(ctx, oxide.InstanceViewParams{
123+
Instance: oxide.NameOrId(instanceID),
124+
})
125+
if err != nil {
126+
return false, fmt.Errorf("failed viewing oxide instance %s: %v", instanceID, err)
127+
}
128+
129+
return instance.RunState == oxide.InstanceStateStopped, nil
130+
}

internal/provider/provider.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package provider
2+
3+
import (
4+
"io"
5+
"os"
6+
7+
"github.com/oxidecomputer/oxide.go/oxide"
8+
"k8s.io/client-go/kubernetes"
9+
cloudprovider "k8s.io/cloud-provider"
10+
"k8s.io/klog/v2"
11+
)
12+
13+
// init registers the Oxide cloud provider as a valid external cloud provider
14+
// for Kubernetes.
15+
func init() {
16+
cloudprovider.RegisterCloudProvider(
17+
Name,
18+
func(config io.Reader) (cloudprovider.Interface, error) {
19+
return &Oxide{}, nil
20+
},
21+
)
22+
}
23+
24+
// Name is the name of this cloud provider.
25+
const Name = "oxide"
26+
27+
var _ cloudprovider.Interface = (*Oxide)(nil)
28+
29+
// Oxide is the Oxide cloud provider. It implements [cloudprovider.Interface] to
30+
// provide Oxide specific functionality.
31+
type Oxide struct {
32+
client *oxide.Client
33+
project string
34+
35+
k8sClient kubernetes.Interface
36+
}
37+
38+
// Initialize creates the Oxide and Kubernetes clients and spawns any additional
39+
// controllers, if necessary.
40+
func (c *Oxide) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
41+
kubernetesClient, err := clientBuilder.Client(Name)
42+
if err != nil {
43+
klog.Fatalf("failed to create kubernetes client: %v", err)
44+
return
45+
}
46+
c.k8sClient = kubernetesClient
47+
48+
oxideClient, err := oxide.NewClient(nil)
49+
if err != nil {
50+
klog.Fatalf("failed to create oxide client: %v", err)
51+
return
52+
}
53+
c.client = oxideClient
54+
55+
c.project = os.Getenv("OXIDE_PROJECT")
56+
57+
klog.InfoS("initialized cloud provider", "type", "oxide")
58+
}
59+
60+
// ProviderName returns the name of this cloud provider.
61+
func (c *Oxide) ProviderName() string {
62+
return Name
63+
}
64+
65+
// HasClusterID is purposefully unimplemented. A cluster ID is used to uniquely
66+
// identify resources for a specific Kubernetes cluster when multiple Kubernetes
67+
// clusters can conflict with each other when using shared resources (e.g.,
68+
// VPC, load balancer). Usually, such resources are tagged or labeled with the
69+
// cluster ID but Oxide does not have resource tags or labels. Additionally,
70+
// it's expected that a Kubernetes cluster on Oxide is deployed in its own VPC
71+
// and does not share resources with other Kubernetes clusters. This may become
72+
// supported in the future when Oxide has resource tags or labels.
73+
func (c *Oxide) HasClusterID() bool {
74+
return false
75+
}
76+
77+
// Clusters is purposefully unimplemented. This is meant for a single Cloud
78+
// Controller Manager to manage multiple Kubernetes clusters but the modern
79+
// idiom is to run a single Cloud Controller Manager per Kubernetes cluster,
80+
// making this irrelevant.
81+
func (c *Oxide) Clusters() (cloudprovider.Clusters, bool) {
82+
return nil, false
83+
}
84+
85+
// Instances is purposefully unimplemented. Use [Oxide.InstancesV2].
86+
func (c *Oxide) Instances() (cloudprovider.Instances, bool) {
87+
return nil, false
88+
}
89+
90+
// InstancesV2 returns an implementation of [cloudprovider.InstancesV2]
91+
// that provides functionality to initialize Kubernetes nodes, provide their
92+
// metadata, and determine whether they exists to facilitate cleanup.
93+
func (c *Oxide) InstancesV2() (cloudprovider.InstancesV2, bool) {
94+
return &InstancesV2{
95+
client: c.client,
96+
project: c.project,
97+
k8sClient: c.k8sClient,
98+
}, true
99+
}
100+
101+
// LoadBalancer is currently unimplemented. This may be implemented in the
102+
// future.
103+
func (c *Oxide) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
104+
return nil, false
105+
}
106+
107+
// Routes is purposefully unimplemented. It is expected that the Kubernetes
108+
// cluster uses a third-party CNI instead of this controller. This may be
109+
// implemented in the future.
110+
func (c *Oxide) Routes() (cloudprovider.Routes, bool) {
111+
return nil, false
112+
}
113+
114+
// Zones is purposefully unimplemented. Zone and region information is retrieved
115+
// from [InstancesV2.InstanceMetadata] instead.
116+
func (c *Oxide) Zones() (cloudprovider.Zones, bool) {
117+
return nil, false
118+
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
_ "k8s.io/component-base/metrics/prometheus/version"
1717
"k8s.io/klog/v2"
1818

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

2222
func main() {

0 commit comments

Comments
 (0)