Skip to content

Commit

Permalink
Initial provider implementation (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
bastjan authored Oct 21, 2024
1 parent 22b96c0 commit 5ef1a6e
Show file tree
Hide file tree
Showing 30 changed files with 4,346 additions and 46 deletions.
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ dist/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Kubernetes Generated files - skip generated files, except for vendored files

!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~

# Temporary vendor directory for manifest sync
.tmpvendor
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ test: ## Run tests
.PHONY: build
build: generate fmt vet $(BIN_FILENAME) ## Build manager binary

.PHONY: sync-crds
sync-crds: ## Sync required openshift CRDs for local testing
go mod vendor -o .tmpvendor
VENDOR_DIR=.tmpvendor ./hack/sync-crds.sh

.PHONY: generate
generate: ## Generate e.g. CRD, RBAC etc.
go generate ./...
go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths="./..."

.PHONY: fmt
fmt: ## Run go fmt against code
Expand All @@ -37,7 +43,7 @@ vet: ## Run go vet against code
go vet ./...

.PHONY: lint
lint: fmt vet generate ## All-in-one linting
lint: fmt vet generate sync-crds ## All-in-one linting
@echo 'Check for uncommitted changes ...'
git diff --exit-code

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

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

.PHONY: run
run: generate fmt vet ## Run a controller from your host.
Expand Down
58 changes: 58 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# machine-api-provider-cloudscale

Provider for cloudscale.ch for the OpenShift machine-api.

## Development

## Updating OCP dependencies

```bash
RELEASE=release-4.XX
go get -u "github.com/openshift/api@${RELEASE}"
go get -u "github.com/openshift/library-go@${RELEASE}"
go get -u "github.com/openshift/machine-api-operator@${RELEASE}"
go mod tidy

# Update the CRDs required for testing on a local non-OCP cluster
make sync-crds
```

### Testing on a local non-OCP cluster

```bash
# Apply required upstream CRDs
kubectl apply -k config/crds

make run

# Apply a generic machine object that will not join a cluster
kubectl apply -f config/samples/machine-cloudscale-generic.yml
```

### Testing on a Project Syn managed OCP cluster

```bash
# Switch to the openshift-machine-api namespace
yq -i '.current-context as $cc | with((.contexts[] | select(.name == $cc) | .context); .namespace = "openshift-machine-api")' ${KUBECONFIG:-$HOME/.kube/config}
# Become system:admin
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}
oc whoami

# Deploy nodelink controller if required
hack/deploy-nodelink-controller.sh

# Generate the userData secret from the main.tf.json in the cluster catalog
./pkg/machine/userdata/userdata-secret-from-maintfjson.sh manifests/openshift4-terraform/main.tf.json | k apply -f-

make run

# Apply a known working machine object
# This will join the cluster and become a worker node
# You want to update:
# - metadata.labels["machine.openshift.io/cluster-api-cluster"]
# - spec.providerSpec.value.zone
# - spec.providerSpec.value.baseDomain
# - spec.providerSpec.value.image
# - spec.providerSpec.value.interfaces[0].networkUUID
kubectl apply -f config/samples/machine-cloudscale-known-working.yml
```
110 changes: 110 additions & 0 deletions api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package v1beta1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type InterfaceType string

const (
// InterfaceTypePublic is a public network interface.
InterfaceTypePublic InterfaceType = "Public"
// InterfaceTypePrivate is a private network interface.
InterfaceTypePrivate InterfaceType = "Private"
)

// CloudscaleMachineProviderSpec is the type that will be embedded in a Machine.Spec.ProviderSpec field
// for a cloudscale virtual machine. It is used by the cloudscale machine actuator to create a single Machine.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type CloudscaleMachineProviderSpec struct {
metav1.TypeMeta `json:",inline"`

// ObjectMeta is the standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
metav1.ObjectMeta `json:"metadata,omitempty"`

// UserDataSecret is a reference to a secret that contains the UserData to apply to the instance.
// The secret must contain a key named userData. The value is evaluated using Jsonnet; it can be either pure JSON or a Jsonnet template.
// The Jsonnet template has access to the following variables:
// - std.extVar('context').machine: the Machine object. The name can be accessed via std.extVar('context').machine.metadata.name for example.
// - std.extVar('context').data: all keys from the UserDataSecret. For example, std.extVar('context').data.foo will access the value of the key foo.
// +optional
UserDataSecret *corev1.LocalObjectReference `json:"userDataSecret,omitempty"`
// TokenSecret is a reference to the secret with the cloudscale API token.
// The secret must contain a key named token.
// If no token is provided, the operator will try to use the default token from CLOUDSCALE_API_TOKEN.
// +optional
TokenSecret *corev1.LocalObjectReference `json:"tokenSecret,omitempty"`

// BaseDomain is the base domain to use for the machine.
// +optional
BaseDomain string `json:"baseDomain,omitempty"`
// Zone is the zone in which the machine will be created.
Zone string `json:"zone"`
// 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.
// The machines are automatically distributed across server groups with the same key.
// +optional
AntiAffinityKey string `json:"antiAffinityKey,omitempty"`
// ServerGroups is a list of UUIDs identifying the server groups to which the new server will be added.
// Used for anti-affinity.
// https://www.cloudscale.ch/en/api/v1#server-groups
ServerGroups []string `json:"serverGroups,omitempty"`
// Tags is a map of tags to apply to the machine.
Tags map[string]string `json:"tags"`
// Flavor is the flavor of the machine.
Flavor string `json:"flavor"`
// Image is the base image to use for the machine.
// For images provided by cloudscale: the image’s slug.
// For custom images: the image’s slug prefixed with custom: (e.g. custom:ubuntu-foo), or its UUID.
// If multiple custom images with the same slug exist, the newest custom image will be used.
// https://www.cloudscale.ch/en/api/v1#images
Image string `json:"image"`
// RootVolumeSizeGB is the size of the root volume in GB.
RootVolumeSizeGB int `json:"rootVolumeSizeGB"`
// SSHKeys is a list of SSH keys to add to the machine.
SSHKeys []string `json:"sshKeys"`
// UseIPV6 is a flag to enable IPv6 on the machine.
// Defaults to true.
UseIPV6 *bool `json:"useIPV6,omitempty"`
// Interfaces is a list of network interfaces to add to the machine.
Interfaces []Interface `json:"interfaces"`
}

// Interface is a network interface to add to a machine.
type Interface struct {
// Type is the type of the interface. Required.
Type InterfaceType `json:"type"`
// NetworkUUID is the UUID of the network to attach the interface to.
// Can only be set if type is private.
// Must be compatible with Addresses.SubnetUUID if both are specified.
NetworkUUID string `json:"networkUUID"`
// Addresses is an optional list of addresses to assign to the interface.
// Can only be set if type is private.
Addresses []Address `json:"addresses"`
}

// Address is an address to assign to a network interface.
type Address struct {
// Address is an optional IP address to assign to the interface.
Address string `json:"address"`
// SubnetUUID is the UUID of the subnet to assign the address to.
// Must be compatible with Interface.NetworkUUID if both are specified.
SubnetUUID string `json:"subnetUUID"`
}

// CloudscaleMachineProviderStatus is the type that will be embedded in a Machine.Status.ProviderStatus field.
// It contains cloudscale-specific status information.
type CloudscaleMachineProviderStatus struct {
metav1.TypeMeta `json:",inline"`

// InstanceID is the ID of the instance in Cloudscale.
// +optional
InstanceID string `json:"instanceId,omitempty"`
// Status is the status of the instance in Cloudscale.
// Can be "changing", "running" or "stopped".
Status string `json:"status,omitempty"`
// Conditions is a set of conditions associated with the Machine to indicate
// errors or other status
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
80 changes: 80 additions & 0 deletions api/cloudscale/provider/v1beta1/conversions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package v1beta1

import (
"encoding/json"
"fmt"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
)

// RawExtensionFromProviderSpec marshals the machine provider spec.
func RawExtensionFromProviderSpec(spec *CloudscaleMachineProviderSpec) (*runtime.RawExtension, error) {
if spec == nil {
return &runtime.RawExtension{}, nil
}

s := spec.DeepCopy()
s.APIVersion = GroupVersion.String()

var rawBytes []byte
var err error
if rawBytes, err = json.Marshal(s); err != nil {
return nil, fmt.Errorf("error marshalling providerSpec: %v", err)
}

return &runtime.RawExtension{
Raw: rawBytes,
}, nil
}

// RawExtensionFromProviderStatus marshals the provider status
func RawExtensionFromProviderStatus(status *CloudscaleMachineProviderStatus) (*runtime.RawExtension, error) {
if status == nil {
return &runtime.RawExtension{}, nil
}

s := status.DeepCopy()
s.APIVersion = GroupVersion.String()

var rawBytes []byte
var err error
if rawBytes, err = json.Marshal(s); err != nil {
return nil, fmt.Errorf("error marshalling providerStatus: %v", err)
}

return &runtime.RawExtension{
Raw: rawBytes,
}, nil
}

// ProviderSpecFromRawExtension unmarshals the JSON-encoded spec
func ProviderSpecFromRawExtension(rawExtension *runtime.RawExtension) (*CloudscaleMachineProviderSpec, error) {
if rawExtension == nil {
return &CloudscaleMachineProviderSpec{}, nil
}

spec := new(CloudscaleMachineProviderSpec)
if err := yaml.Unmarshal(rawExtension.Raw, &spec); err != nil {
return nil, fmt.Errorf("error unmarshalling providerSpec: %v", err)
}

klog.V(5).Infof("Got provider spec from raw extension: %+v", spec)
return spec, nil
}

// ProviderStatusFromRawExtension unmarshals a raw extension into a GCPMachineProviderStatus type
func ProviderStatusFromRawExtension(rawExtension *runtime.RawExtension) (*CloudscaleMachineProviderStatus, error) {
if rawExtension == nil {
return &CloudscaleMachineProviderStatus{}, nil
}

providerStatus := new(CloudscaleMachineProviderStatus)
if err := yaml.Unmarshal(rawExtension.Raw, providerStatus); err != nil {
return nil, fmt.Errorf("error unmarshalling providerStatus: %v", err)
}

klog.V(5).Infof("Got provider Status from raw extension: %+v", providerStatus)
return providerStatus, nil
}
12 changes: 12 additions & 0 deletions api/cloudscale/provider/v1beta1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package v1beta1

import "k8s.io/apimachinery/pkg/runtime/schema"

// +k8s:deepcopy-gen=package
// +k8s:defaulter-gen=TypeMeta
// +k8s:openapi-gen=true

var (
GroupName = "machine.appuio.io"
GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"}
)
Loading

0 comments on commit 5ef1a6e

Please sign in to comment.