Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial provider implementation #2

Merged
merged 22 commits into from
Oct 21, 2024
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
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