Skip to content

Commit 5ef1a6e

Browse files
authored
Initial provider implementation (#2)
1 parent 22b96c0 commit 5ef1a6e

30 files changed

+4346
-46
lines changed

.gitignore

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ dist/
2020
# Output of the go coverage tool, specifically when used with LiteIDE
2121
*.out
2222

23-
# Kubernetes Generated files - skip generated files, except for vendored files
24-
25-
!vendor/**/zz_generated.*
26-
2723
# editor and IDE paraphernalia
2824
.idea
2925
*.swp
3026
*.swo
3127
*~
28+
29+
# Temporary vendor directory for manifest sync
30+
.tmpvendor

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ test: ## Run tests
2424
.PHONY: build
2525
build: generate fmt vet $(BIN_FILENAME) ## Build manager binary
2626

27+
.PHONY: sync-crds
28+
sync-crds: ## Sync required openshift CRDs for local testing
29+
go mod vendor -o .tmpvendor
30+
VENDOR_DIR=.tmpvendor ./hack/sync-crds.sh
31+
2732
.PHONY: generate
2833
generate: ## Generate e.g. CRD, RBAC etc.
2934
go generate ./...
35+
go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths="./..."
3036

3137
.PHONY: fmt
3238
fmt: ## Run go fmt against code
@@ -37,7 +43,7 @@ vet: ## Run go vet against code
3743
go vet ./...
3844

3945
.PHONY: lint
40-
lint: fmt vet generate ## All-in-one linting
46+
lint: fmt vet generate sync-crds ## All-in-one linting
4147
@echo 'Check for uncommitted changes ...'
4248
git diff --exit-code
4349

@@ -48,6 +54,7 @@ build.docker: $(BIN_FILENAME) ## Build the docker image
4854

4955
clean: ## Cleans up the generated resources
5056
rm -rf dist/ cover.out $(BIN_FILENAME) || true
57+
rm -rf .tmpvendor
5158

5259
.PHONY: run
5360
run: generate fmt vet ## Run a controller from your host.

Readme.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# machine-api-provider-cloudscale
2+
3+
Provider for cloudscale.ch for the OpenShift machine-api.
4+
5+
## Development
6+
7+
## Updating OCP dependencies
8+
9+
```bash
10+
RELEASE=release-4.XX
11+
go get -u "github.com/openshift/api@${RELEASE}"
12+
go get -u "github.com/openshift/library-go@${RELEASE}"
13+
go get -u "github.com/openshift/machine-api-operator@${RELEASE}"
14+
go mod tidy
15+
16+
# Update the CRDs required for testing on a local non-OCP cluster
17+
make sync-crds
18+
```
19+
20+
### Testing on a local non-OCP cluster
21+
22+
```bash
23+
# Apply required upstream CRDs
24+
kubectl apply -k config/crds
25+
26+
make run
27+
28+
# Apply a generic machine object that will not join a cluster
29+
kubectl apply -f config/samples/machine-cloudscale-generic.yml
30+
```
31+
32+
### Testing on a Project Syn managed OCP cluster
33+
34+
```bash
35+
# Switch to the openshift-machine-api namespace
36+
yq -i '.current-context as $cc | with((.contexts[] | select(.name == $cc) | .context); .namespace = "openshift-machine-api")' ${KUBECONFIG:-$HOME/.kube/config}
37+
# Become system:admin
38+
yq -i '.current-context as $cc | (.contexts[] | select(.name == $cc) | .context.user) as $cu | with(.users[] | select(.name == $cu); .user.as = "system:admin")' ${KUBECONFIG:-$HOME/.kube/config}
39+
oc whoami
40+
41+
# Deploy nodelink controller if required
42+
hack/deploy-nodelink-controller.sh
43+
44+
# Generate the userData secret from the main.tf.json in the cluster catalog
45+
./pkg/machine/userdata/userdata-secret-from-maintfjson.sh manifests/openshift4-terraform/main.tf.json | k apply -f-
46+
47+
make run
48+
49+
# Apply a known working machine object
50+
# This will join the cluster and become a worker node
51+
# You want to update:
52+
# - metadata.labels["machine.openshift.io/cluster-api-cluster"]
53+
# - spec.providerSpec.value.zone
54+
# - spec.providerSpec.value.baseDomain
55+
# - spec.providerSpec.value.image
56+
# - spec.providerSpec.value.interfaces[0].networkUUID
57+
kubectl apply -f config/samples/machine-cloudscale-known-working.yml
58+
```
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package v1beta1
2+
3+
import (
4+
corev1 "k8s.io/api/core/v1"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
)
7+
8+
type InterfaceType string
9+
10+
const (
11+
// InterfaceTypePublic is a public network interface.
12+
InterfaceTypePublic InterfaceType = "Public"
13+
// InterfaceTypePrivate is a private network interface.
14+
InterfaceTypePrivate InterfaceType = "Private"
15+
)
16+
17+
// CloudscaleMachineProviderSpec is the type that will be embedded in a Machine.Spec.ProviderSpec field
18+
// for a cloudscale virtual machine. It is used by the cloudscale machine actuator to create a single Machine.
19+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
20+
type CloudscaleMachineProviderSpec struct {
21+
metav1.TypeMeta `json:",inline"`
22+
23+
// ObjectMeta is the standard object's metadata.
24+
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
25+
metav1.ObjectMeta `json:"metadata,omitempty"`
26+
27+
// UserDataSecret is a reference to a secret that contains the UserData to apply to the instance.
28+
// The secret must contain a key named userData. The value is evaluated using Jsonnet; it can be either pure JSON or a Jsonnet template.
29+
// The Jsonnet template has access to the following variables:
30+
// - std.extVar('context').machine: the Machine object. The name can be accessed via std.extVar('context').machine.metadata.name for example.
31+
// - std.extVar('context').data: all keys from the UserDataSecret. For example, std.extVar('context').data.foo will access the value of the key foo.
32+
// +optional
33+
UserDataSecret *corev1.LocalObjectReference `json:"userDataSecret,omitempty"`
34+
// TokenSecret is a reference to the secret with the cloudscale API token.
35+
// The secret must contain a key named token.
36+
// If no token is provided, the operator will try to use the default token from CLOUDSCALE_API_TOKEN.
37+
// +optional
38+
TokenSecret *corev1.LocalObjectReference `json:"tokenSecret,omitempty"`
39+
40+
// BaseDomain is the base domain to use for the machine.
41+
// +optional
42+
BaseDomain string `json:"baseDomain,omitempty"`
43+
// Zone is the zone in which the machine will be created.
44+
Zone string `json:"zone"`
45+
// AntiAffinityKey is a key to use for anti-affinity. If set, the machine will be placed in different cloudscale server groups based on this key.
46+
// The machines are automatically distributed across server groups with the same key.
47+
// +optional
48+
AntiAffinityKey string `json:"antiAffinityKey,omitempty"`
49+
// ServerGroups is a list of UUIDs identifying the server groups to which the new server will be added.
50+
// Used for anti-affinity.
51+
// https://www.cloudscale.ch/en/api/v1#server-groups
52+
ServerGroups []string `json:"serverGroups,omitempty"`
53+
// Tags is a map of tags to apply to the machine.
54+
Tags map[string]string `json:"tags"`
55+
// Flavor is the flavor of the machine.
56+
Flavor string `json:"flavor"`
57+
// Image is the base image to use for the machine.
58+
// For images provided by cloudscale: the image’s slug.
59+
// For custom images: the image’s slug prefixed with custom: (e.g. custom:ubuntu-foo), or its UUID.
60+
// If multiple custom images with the same slug exist, the newest custom image will be used.
61+
// https://www.cloudscale.ch/en/api/v1#images
62+
Image string `json:"image"`
63+
// RootVolumeSizeGB is the size of the root volume in GB.
64+
RootVolumeSizeGB int `json:"rootVolumeSizeGB"`
65+
// SSHKeys is a list of SSH keys to add to the machine.
66+
SSHKeys []string `json:"sshKeys"`
67+
// UseIPV6 is a flag to enable IPv6 on the machine.
68+
// Defaults to true.
69+
UseIPV6 *bool `json:"useIPV6,omitempty"`
70+
// Interfaces is a list of network interfaces to add to the machine.
71+
Interfaces []Interface `json:"interfaces"`
72+
}
73+
74+
// Interface is a network interface to add to a machine.
75+
type Interface struct {
76+
// Type is the type of the interface. Required.
77+
Type InterfaceType `json:"type"`
78+
// NetworkUUID is the UUID of the network to attach the interface to.
79+
// Can only be set if type is private.
80+
// Must be compatible with Addresses.SubnetUUID if both are specified.
81+
NetworkUUID string `json:"networkUUID"`
82+
// Addresses is an optional list of addresses to assign to the interface.
83+
// Can only be set if type is private.
84+
Addresses []Address `json:"addresses"`
85+
}
86+
87+
// Address is an address to assign to a network interface.
88+
type Address struct {
89+
// Address is an optional IP address to assign to the interface.
90+
Address string `json:"address"`
91+
// SubnetUUID is the UUID of the subnet to assign the address to.
92+
// Must be compatible with Interface.NetworkUUID if both are specified.
93+
SubnetUUID string `json:"subnetUUID"`
94+
}
95+
96+
// CloudscaleMachineProviderStatus is the type that will be embedded in a Machine.Status.ProviderStatus field.
97+
// It contains cloudscale-specific status information.
98+
type CloudscaleMachineProviderStatus struct {
99+
metav1.TypeMeta `json:",inline"`
100+
101+
// InstanceID is the ID of the instance in Cloudscale.
102+
// +optional
103+
InstanceID string `json:"instanceId,omitempty"`
104+
// Status is the status of the instance in Cloudscale.
105+
// Can be "changing", "running" or "stopped".
106+
Status string `json:"status,omitempty"`
107+
// Conditions is a set of conditions associated with the Machine to indicate
108+
// errors or other status
109+
Conditions []metav1.Condition `json:"conditions,omitempty"`
110+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package v1beta1
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"k8s.io/apimachinery/pkg/runtime"
8+
"k8s.io/klog/v2"
9+
"sigs.k8s.io/yaml"
10+
)
11+
12+
// RawExtensionFromProviderSpec marshals the machine provider spec.
13+
func RawExtensionFromProviderSpec(spec *CloudscaleMachineProviderSpec) (*runtime.RawExtension, error) {
14+
if spec == nil {
15+
return &runtime.RawExtension{}, nil
16+
}
17+
18+
s := spec.DeepCopy()
19+
s.APIVersion = GroupVersion.String()
20+
21+
var rawBytes []byte
22+
var err error
23+
if rawBytes, err = json.Marshal(s); err != nil {
24+
return nil, fmt.Errorf("error marshalling providerSpec: %v", err)
25+
}
26+
27+
return &runtime.RawExtension{
28+
Raw: rawBytes,
29+
}, nil
30+
}
31+
32+
// RawExtensionFromProviderStatus marshals the provider status
33+
func RawExtensionFromProviderStatus(status *CloudscaleMachineProviderStatus) (*runtime.RawExtension, error) {
34+
if status == nil {
35+
return &runtime.RawExtension{}, nil
36+
}
37+
38+
s := status.DeepCopy()
39+
s.APIVersion = GroupVersion.String()
40+
41+
var rawBytes []byte
42+
var err error
43+
if rawBytes, err = json.Marshal(s); err != nil {
44+
return nil, fmt.Errorf("error marshalling providerStatus: %v", err)
45+
}
46+
47+
return &runtime.RawExtension{
48+
Raw: rawBytes,
49+
}, nil
50+
}
51+
52+
// ProviderSpecFromRawExtension unmarshals the JSON-encoded spec
53+
func ProviderSpecFromRawExtension(rawExtension *runtime.RawExtension) (*CloudscaleMachineProviderSpec, error) {
54+
if rawExtension == nil {
55+
return &CloudscaleMachineProviderSpec{}, nil
56+
}
57+
58+
spec := new(CloudscaleMachineProviderSpec)
59+
if err := yaml.Unmarshal(rawExtension.Raw, &spec); err != nil {
60+
return nil, fmt.Errorf("error unmarshalling providerSpec: %v", err)
61+
}
62+
63+
klog.V(5).Infof("Got provider spec from raw extension: %+v", spec)
64+
return spec, nil
65+
}
66+
67+
// ProviderStatusFromRawExtension unmarshals a raw extension into a GCPMachineProviderStatus type
68+
func ProviderStatusFromRawExtension(rawExtension *runtime.RawExtension) (*CloudscaleMachineProviderStatus, error) {
69+
if rawExtension == nil {
70+
return &CloudscaleMachineProviderStatus{}, nil
71+
}
72+
73+
providerStatus := new(CloudscaleMachineProviderStatus)
74+
if err := yaml.Unmarshal(rawExtension.Raw, providerStatus); err != nil {
75+
return nil, fmt.Errorf("error unmarshalling providerStatus: %v", err)
76+
}
77+
78+
klog.V(5).Infof("Got provider Status from raw extension: %+v", providerStatus)
79+
return providerStatus, nil
80+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package v1beta1
2+
3+
import "k8s.io/apimachinery/pkg/runtime/schema"
4+
5+
// +k8s:deepcopy-gen=package
6+
// +k8s:defaulter-gen=TypeMeta
7+
// +k8s:openapi-gen=true
8+
9+
var (
10+
GroupName = "machine.appuio.io"
11+
GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
12+
)

0 commit comments

Comments
 (0)