From 5ef1a6e30f3290d3dcd326cca7f6dd4cea6b544e Mon Sep 17 00:00:00 2001 From: Sebastian Widmer Date: Mon, 21 Oct 2024 13:36:29 +0200 Subject: [PATCH] Initial provider implementation (#2) --- .gitignore | 7 +- Makefile | 9 +- Readme.md | 58 ++ .../v1beta1/cloudscaleprovider_types.go | 110 +++ .../provider/v1beta1/conversions.go | 80 ++ api/cloudscale/provider/v1beta1/doc.go | 12 + .../provider/v1beta1/zz_generated.deepcopy.go | 133 +++ config/crds/kustomization.yaml | 4 + config/crds/machine.openshift.io.crd.yaml | 497 +++++++++++ .../machinehealthcheck.openshift.io.crd.yaml | 270 ++++++ config/crds/machineset.openshift.io.crd.yaml | 611 ++++++++++++++ config/samples/machine-cloudscale-generic.yml | 26 + .../machine-cloudscale-known-working.yml | 40 + controllers/machineset_controller.go | 150 ++++ controllers/machineset_controller_test.go | 64 ++ go.mod | 56 +- go.sum | 446 +++++++++- hack/deploy-nodelink-controller.sh | 37 + hack/nodelink-controller.yaml | 38 + hack/sync-crds.sh | 13 + main.go | 103 ++- pkg/machine/actuator.go | 489 +++++++++++ pkg/machine/actuator_test.go | 773 ++++++++++++++++++ pkg/machine/csmock/server_group_service.go | 120 +++ pkg/machine/csmock/server_service.go | 162 ++++ pkg/machine/mock.go | 4 + pkg/machine/userdata/secret.jsonnet | 14 + .../userdata-secret-from-maintfjson.sh | 13 + pkg/machine/userdata/userdata.jsonnet | 36 + tools.go | 17 + 30 files changed, 4346 insertions(+), 46 deletions(-) create mode 100644 Readme.md create mode 100644 api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go create mode 100644 api/cloudscale/provider/v1beta1/conversions.go create mode 100644 api/cloudscale/provider/v1beta1/doc.go create mode 100644 api/cloudscale/provider/v1beta1/zz_generated.deepcopy.go create mode 100644 config/crds/kustomization.yaml create mode 100644 config/crds/machine.openshift.io.crd.yaml create mode 100644 config/crds/machinehealthcheck.openshift.io.crd.yaml create mode 100644 config/crds/machineset.openshift.io.crd.yaml create mode 100644 config/samples/machine-cloudscale-generic.yml create mode 100644 config/samples/machine-cloudscale-known-working.yml create mode 100644 controllers/machineset_controller.go create mode 100644 controllers/machineset_controller_test.go create mode 100755 hack/deploy-nodelink-controller.sh create mode 100644 hack/nodelink-controller.yaml create mode 100755 hack/sync-crds.sh create mode 100644 pkg/machine/actuator.go create mode 100644 pkg/machine/actuator_test.go create mode 100644 pkg/machine/csmock/server_group_service.go create mode 100644 pkg/machine/csmock/server_service.go create mode 100644 pkg/machine/mock.go create mode 100644 pkg/machine/userdata/secret.jsonnet create mode 100755 pkg/machine/userdata/userdata-secret-from-maintfjson.sh create mode 100644 pkg/machine/userdata/userdata.jsonnet create mode 100644 tools.go diff --git a/.gitignore b/.gitignore index 34c3696..1c4465c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 69e8bd7..2e6070d 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 @@ -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. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..1f85379 --- /dev/null +++ b/Readme.md @@ -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 +``` diff --git a/api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go b/api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go new file mode 100644 index 0000000..7af6fbf --- /dev/null +++ b/api/cloudscale/provider/v1beta1/cloudscaleprovider_types.go @@ -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"` +} diff --git a/api/cloudscale/provider/v1beta1/conversions.go b/api/cloudscale/provider/v1beta1/conversions.go new file mode 100644 index 0000000..6738e84 --- /dev/null +++ b/api/cloudscale/provider/v1beta1/conversions.go @@ -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 +} diff --git a/api/cloudscale/provider/v1beta1/doc.go b/api/cloudscale/provider/v1beta1/doc.go new file mode 100644 index 0000000..6aac79a --- /dev/null +++ b/api/cloudscale/provider/v1beta1/doc.go @@ -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"} +) diff --git a/api/cloudscale/provider/v1beta1/zz_generated.deepcopy.go b/api/cloudscale/provider/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000..6b81508 --- /dev/null +++ b/api/cloudscale/provider/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,133 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Address) DeepCopyInto(out *Address) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. +func (in *Address) DeepCopy() *Address { + if in == nil { + return nil + } + out := new(Address) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudscaleMachineProviderSpec) DeepCopyInto(out *CloudscaleMachineProviderSpec) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.UserDataSecret != nil { + in, out := &in.UserDataSecret, &out.UserDataSecret + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.TokenSecret != nil { + in, out := &in.TokenSecret, &out.TokenSecret + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.ServerGroups != nil { + in, out := &in.ServerGroups, &out.ServerGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.SSHKeys != nil { + in, out := &in.SSHKeys, &out.SSHKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UseIPV6 != nil { + in, out := &in.UseIPV6, &out.UseIPV6 + *out = new(bool) + **out = **in + } + if in.Interfaces != nil { + in, out := &in.Interfaces, &out.Interfaces + *out = make([]Interface, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudscaleMachineProviderSpec. +func (in *CloudscaleMachineProviderSpec) DeepCopy() *CloudscaleMachineProviderSpec { + if in == nil { + return nil + } + out := new(CloudscaleMachineProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudscaleMachineProviderSpec) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudscaleMachineProviderStatus) DeepCopyInto(out *CloudscaleMachineProviderStatus) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudscaleMachineProviderStatus. +func (in *CloudscaleMachineProviderStatus) DeepCopy() *CloudscaleMachineProviderStatus { + if in == nil { + return nil + } + out := new(CloudscaleMachineProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Interface) DeepCopyInto(out *Interface) { + *out = *in + if in.Addresses != nil { + in, out := &in.Addresses, &out.Addresses + *out = make([]Address, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Interface. +func (in *Interface) DeepCopy() *Interface { + if in == nil { + return nil + } + out := new(Interface) + in.DeepCopyInto(out) + return out +} diff --git a/config/crds/kustomization.yaml b/config/crds/kustomization.yaml new file mode 100644 index 0000000..1513c44 --- /dev/null +++ b/config/crds/kustomization.yaml @@ -0,0 +1,4 @@ +resources: +- machine.openshift.io.crd.yaml +- machinehealthcheck.openshift.io.crd.yaml +- machineset.openshift.io.crd.yaml diff --git a/config/crds/machine.openshift.io.crd.yaml b/config/crds/machine.openshift.io.crd.yaml new file mode 100644 index 0000000..c3ed7f2 --- /dev/null +++ b/config/crds/machine.openshift.io.crd.yaml @@ -0,0 +1,497 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/948 + api.openshift.io/merged-by-featuregates: "true" + capability.openshift.io/name: MachineAPI + exclude.release.openshift.io/internal-openshift-hosted: "true" + include.release.openshift.io/self-managed-high-availability: "true" + release.openshift.io/feature-set: Default + name: machines.machine.openshift.io +spec: + group: machine.openshift.io + names: + kind: Machine + listKind: MachineList + plural: machines + singular: machine + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Phase of machine + jsonPath: .status.phase + name: Phase + type: string + - description: Type of instance + jsonPath: .metadata.labels['machine\.openshift\.io/instance-type'] + name: Type + type: string + - description: Region associated with machine + jsonPath: .metadata.labels['machine\.openshift\.io/region'] + name: Region + type: string + - description: Zone associated with machine + jsonPath: .metadata.labels['machine\.openshift\.io/zone'] + name: Zone + type: string + - description: Machine age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Node associated with machine + jsonPath: .status.nodeRef.name + name: Node + priority: 1 + type: string + - description: Provider ID of machine created in cloud provider + jsonPath: .spec.providerID + name: ProviderID + priority: 1 + type: string + - description: State of instance + jsonPath: .metadata.annotations['machine\.openshift\.io/instance-state'] + name: State + priority: 1 + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: 'Machine is the Schema for the machines API Compatibility level + 2: Stable within a major release for a minimum of 9 months or 3 minor releases + (whichever is longer).' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MachineSpec defines the desired state of Machine + properties: + lifecycleHooks: + description: LifecycleHooks allow users to pause operations on the + machine at certain predefined points within the machine lifecycle. + properties: + preDrain: + description: PreDrain hooks prevent the machine from being drained. + This also blocks further lifecycle events, such as termination. + items: + description: LifecycleHook represents a single instance of a + lifecycle hook + properties: + name: + description: Name defines a unique name for the lifcycle + hook. The name should be unique and descriptive, ideally + 1-3 words, in CamelCase or it may be namespaced, eg. foo.example.com/CamelCase. + Names must be unique and should only be managed by a single + entity. + maxLength: 256 + minLength: 3 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + owner: + description: Owner defines the owner of the lifecycle hook. + This should be descriptive enough so that users can identify + who/what is responsible for blocking the lifecycle. This + could be the name of a controller (e.g. clusteroperator/etcd) + or an administrator managing the hook. + maxLength: 512 + minLength: 3 + type: string + required: + - name + - owner + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + preTerminate: + description: PreTerminate hooks prevent the machine from being + terminated. PreTerminate hooks be actioned after the Machine + has been drained. + items: + description: LifecycleHook represents a single instance of a + lifecycle hook + properties: + name: + description: Name defines a unique name for the lifcycle + hook. The name should be unique and descriptive, ideally + 1-3 words, in CamelCase or it may be namespaced, eg. foo.example.com/CamelCase. + Names must be unique and should only be managed by a single + entity. + maxLength: 256 + minLength: 3 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + owner: + description: Owner defines the owner of the lifecycle hook. + This should be descriptive enough so that users can identify + who/what is responsible for blocking the lifecycle. This + could be the name of a controller (e.g. clusteroperator/etcd) + or an administrator managing the hook. + maxLength: 512 + minLength: 3 + type: string + required: + - name + - owner + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + metadata: + description: ObjectMeta will autopopulate the Node created. Use this + to indicate what labels, annotations, name prefix, etc., should + be used when creating the Node. + properties: + annotations: + additionalProperties: + type: string + description: 'Annotations is an unstructured key value map stored + with a resource that may be set by external tools to store and + retrieve arbitrary metadata. They are not queryable and should + be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations' + type: object + generateName: + description: "GenerateName is an optional prefix, used by the + server, to generate a unique name ONLY IF the Name field has + not been provided. If this field is used, the name returned + to the client will be different than the name passed. This value + will also be combined with a unique suffix. The provided value + has the same validation rules as the Name field, and may be + truncated by the length of the suffix required to make the value + unique on the server. \n If this field is specified and the + generated name exists, the server will NOT return a 409 - instead, + it will either return 201 Created or 500 with Reason ServerTimeout + indicating a unique name could not be found in the time allotted, + and the client should retry (optionally after the time indicated + in the Retry-After header). \n Applied only if Name is not specified. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency" + type: string + labels: + additionalProperties: + type: string + description: 'Map of string keys and values that can be used to + organize and categorize (scope and select) objects. May match + selectors of replication controllers and services. More info: + http://kubernetes.io/docs/user-guide/labels' + type: object + name: + description: 'Name must be unique within a namespace. Is required + when creating resources, although some resources may allow a + client to request the generation of an appropriate name automatically. + Name is primarily intended for creation idempotence and configuration + definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + namespace: + description: "Namespace defines the space within each name must + be unique. An empty namespace is equivalent to the \"default\" + namespace, but \"default\" is the canonical representation. + Not all objects are required to be scoped to a namespace - the + value of this field for those objects will be empty. \n Must + be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces" + type: string + ownerReferences: + description: List of objects depended by this object. If ALL objects + in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in + this list will point to this controller, with the controller + field set to true. There cannot be more than one managing controller. + items: + description: OwnerReference contains enough information to let + you identify an owning object. An owning object must be in + the same namespace as the dependent, or be cluster-scoped, + so there is no namespace field. + properties: + apiVersion: + description: API version of the referent. + type: string + blockOwnerDeletion: + description: If true, AND if the owner has the "foregroundDeletion" + finalizer, then the owner cannot be deleted from the key-value + store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion + for how the garbage collector interacts with this field + and enforces the foreground deletion. Defaults to false. + To set this field, a user needs "delete" permission of + the owner, otherwise 422 (Unprocessable Entity) will be + returned. + type: boolean + controller: + description: If true, this reference points to the managing + controller. + type: boolean + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids' + type: string + required: + - apiVersion + - kind + - name + - uid + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - uid + x-kubernetes-list-type: map + type: object + providerID: + description: ProviderID is the identification ID of the machine provided + by the provider. This field must match the provider ID as seen on + the node object corresponding to this machine. This field is required + by higher level consumers of cluster-api. Example use case is cluster + autoscaler with cluster-api as provider. Clean-up logic in the autoscaler + compares machines to nodes to find out machines at provider which + could not get registered as Kubernetes nodes. With cluster-api as + a generic out-of-tree provider for autoscaler, this field is required + by autoscaler to be able to have a provider view of the list of + machines. Another list of nodes is queried from the k8s apiserver + and then a comparison is done to find out unregistered machines + and are marked for delete. This field will be set by the actuators + and consumed by higher level entities like autoscaler that will + be interfacing with cluster-api as generic provider. + type: string + providerSpec: + description: ProviderSpec details Provider-specific configuration + to use during node creation. + properties: + value: + description: Value is an inlined, serialized representation of + the resource configuration. It is recommended that providers + maintain their own versioned API types that should be serialized/deserialized + from this field, akin to component config. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + taints: + description: The list of the taints to be applied to the corresponding + Node in additive manner. This list will not overwrite any other + taints added to the Node on an ongoing basis by other entities. + These taints should be actively reconciled e.g. if you ask the machine + controller to apply a taint and then manually remove the taint the + machine controller will put it back) but not have the machine controller + remove any taints + items: + description: The node this Taint is attached to has the "effect" + on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods that + do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule + and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the taint + was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint key. + type: string + required: + - effect + - key + type: object + type: array + x-kubernetes-list-type: atomic + type: object + status: + description: MachineStatus defines the observed state of Machine + properties: + addresses: + description: Addresses is a list of addresses assigned to the machine. + Queried from cloud provider, if available. + items: + description: NodeAddress contains information for the node's address. + properties: + address: + description: The node address. + type: string + type: + description: Node address type, one of Hostname, ExternalIP + or InternalIP. + type: string + required: + - address + - type + type: object + type: array + x-kubernetes-list-type: atomic + conditions: + description: Conditions defines the current state of the Machine + items: + description: Condition defines an observation of a Machine API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + errorMessage: + description: "ErrorMessage will be set in the event that there is + a terminal problem reconciling the Machine and will contain a more + verbose string suitable for logging and human consumption. \n This + field should not be set for transitive errors that a controller + faces that are expected to be fixed automatically over time (like + service outages), but instead indicate that something is fundamentally + wrong with the Machine's spec or the configuration of the controller, + and that manual intervention is required. Examples of terminal errors + would be invalid combinations of settings in the spec, values that + are unsupported by the controller, or the responsible controller + itself being critically misconfigured. \n Any transient errors that + occur during the reconciliation of Machines can be added as events + to the Machine object and/or logged in the controller's output." + type: string + errorReason: + description: "ErrorReason will be set in the event that there is a + terminal problem reconciling the Machine and will contain a succinct + value suitable for machine interpretation. \n This field should + not be set for transitive errors that a controller faces that are + expected to be fixed automatically over time (like service outages), + but instead indicate that something is fundamentally wrong with + the Machine's spec or the configuration of the controller, and that + manual intervention is required. Examples of terminal errors would + be invalid combinations of settings in the spec, values that are + unsupported by the controller, or the responsible controller itself + being critically misconfigured. \n Any transient errors that occur + during the reconciliation of Machines can be added as events to + the Machine object and/or logged in the controller's output." + type: string + lastOperation: + description: LastOperation describes the last-operation performed + by the machine-controller. This API should be useful as a history + in terms of the latest operation performed on the specific machine. + It should also convey the state of the latest-operation for example + if it is still on-going, failed or completed successfully. + properties: + description: + description: Description is the human-readable description of + the last operation. + type: string + lastUpdated: + description: LastUpdated is the timestamp at which LastOperation + API was last-updated. + format: date-time + type: string + state: + description: State is the current status of the last performed + operation. E.g. Processing, Failed, Successful etc + type: string + type: + description: Type is the type of operation which was last performed. + E.g. Create, Delete, Update etc + type: string + type: object + lastUpdated: + description: LastUpdated identifies when this status was last observed. + format: date-time + type: string + nodeRef: + description: NodeRef will point to the corresponding Node if it exists. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + phase: + description: 'Phase represents the current phase of machine actuation. + One of: Failed, Provisioning, Provisioned, Running, Deleting' + type: string + providerStatus: + description: ProviderStatus details a Provider-specific status. It + is recommended that providers maintain their own versioned API types + that should be serialized/deserialized from this field. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crds/machinehealthcheck.openshift.io.crd.yaml b/config/crds/machinehealthcheck.openshift.io.crd.yaml new file mode 100644 index 0000000..41bc3f1 --- /dev/null +++ b/config/crds/machinehealthcheck.openshift.io.crd.yaml @@ -0,0 +1,270 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1032 + api.openshift.io/merged-by-featuregates: "true" + capability.openshift.io/name: MachineAPI + exclude.release.openshift.io/internal-openshift-hosted: "true" + include.release.openshift.io/self-managed-high-availability: "true" + name: machinehealthchecks.machine.openshift.io +spec: + group: machine.openshift.io + names: + kind: MachineHealthCheck + listKind: MachineHealthCheckList + plural: machinehealthchecks + shortNames: + - mhc + - mhcs + singular: machinehealthcheck + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Maximum number of unhealthy machines allowed + jsonPath: .spec.maxUnhealthy + name: MaxUnhealthy + type: string + - description: Number of machines currently monitored + jsonPath: .status.expectedMachines + name: ExpectedMachines + type: integer + - description: Current observed healthy machines + jsonPath: .status.currentHealthy + name: CurrentHealthy + type: integer + name: v1beta1 + schema: + openAPIV3Schema: + description: 'MachineHealthCheck is the Schema for the machinehealthchecks + API Compatibility level 2: Stable within a major release for a minimum of + 9 months or 3 minor releases (whichever is longer).' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Specification of machine health check policy + properties: + maxUnhealthy: + anyOf: + - type: integer + - type: string + default: 100% + description: Any farther remediation is only allowed if at most "MaxUnhealthy" + machines selected by "selector" are not healthy. Expects either + a postive integer value or a percentage value. Percentage values + must be positive whole numbers and are capped at 100%. Both 0 and + 0% are valid and will block all remediation. + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + nodeStartupTimeout: + default: 10m + description: Machines older than this duration without a node will + be considered to have failed and will be remediated. To prevent + Machines without Nodes from being removed, disable startup checks + by setting this value explicitly to "0". Expects an unsigned duration + string of decimal numbers each with optional fraction and a unit + suffix, eg "300ms", "1.5h" or "2h45m". Valid time units are "ns", + "us" (or "µs"), "ms", "s", "m", "h". + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + remediationTemplate: + description: "RemediationTemplate is a reference to a remediation + template provided by an infrastructure provider. \n This field is + completely optional, when filled, the MachineHealthCheck controller + creates a new object from the template referenced and hands off + remediation of the machine to a controller that lives outside of + Machine API Operator." + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + selector: + description: 'Label selector to match machines whose health will be + exercised. Note: An empty selector will match all machines.' + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + unhealthyConditions: + description: UnhealthyConditions contains a list of the conditions + that determine whether a node is considered unhealthy. The conditions + are combined in a logical OR, i.e. if any of the conditions is met, + the node is unhealthy. + items: + description: UnhealthyCondition represents a Node condition type + and value with a timeout specified as a duration. When the named + condition has been in the given status for at least the timeout + value, a node is considered unhealthy. + properties: + status: + minLength: 1 + type: string + timeout: + description: Expects an unsigned duration string of decimal + numbers each with optional fraction and a unit suffix, eg + "300ms", "1.5h" or "2h45m". Valid time units are "ns", "us" + (or "µs"), "ms", "s", "m", "h". + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + type: + minLength: 1 + type: string + type: object + minItems: 1 + type: array + type: object + status: + description: Most recently observed status of MachineHealthCheck resource + properties: + conditions: + description: Conditions defines the current state of the MachineHealthCheck + items: + description: Condition defines an observation of a Machine API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentHealthy: + description: total number of machines counted by this machine health + check + minimum: 0 + type: integer + expectedMachines: + description: total number of machines counted by this machine health + check + minimum: 0 + type: integer + remediationsAllowed: + description: RemediationsAllowed is the number of further remediations + allowed by this machine health check before maxUnhealthy short circuiting + will be applied + format: int32 + minimum: 0 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crds/machineset.openshift.io.crd.yaml b/config/crds/machineset.openshift.io.crd.yaml new file mode 100644 index 0000000..f996db2 --- /dev/null +++ b/config/crds/machineset.openshift.io.crd.yaml @@ -0,0 +1,611 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1032 + api.openshift.io/merged-by-featuregates: "true" + capability.openshift.io/name: MachineAPI + exclude.release.openshift.io/internal-openshift-hosted: "true" + include.release.openshift.io/self-managed-high-availability: "true" + release.openshift.io/feature-set: Default + name: machinesets.machine.openshift.io +spec: + group: machine.openshift.io + names: + kind: MachineSet + listKind: MachineSetList + plural: machinesets + singular: machineset + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Desired Replicas + jsonPath: .spec.replicas + name: Desired + type: integer + - description: Current Replicas + jsonPath: .status.replicas + name: Current + type: integer + - description: Ready Replicas + jsonPath: .status.readyReplicas + name: Ready + type: integer + - description: Observed number of available replicas + jsonPath: .status.availableReplicas + name: Available + type: string + - description: Machineset age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: 'MachineSet ensures that a specified number of machines replicas + are running at any given time. Compatibility level 2: Stable within a major + release for a minimum of 9 months or 3 minor releases (whichever is longer).' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MachineSetSpec defines the desired state of MachineSet + properties: + deletePolicy: + description: DeletePolicy defines the policy used to identify nodes + to delete when downscaling. Defaults to "Random". Valid values + are "Random, "Newest", "Oldest" + enum: + - Random + - Newest + - Oldest + type: string + minReadySeconds: + description: MinReadySeconds is the minimum number of seconds for + which a newly created machine should be ready. Defaults to 0 (machine + will be considered available as soon as it is ready) + format: int32 + type: integer + replicas: + default: 1 + description: Replicas is the number of desired replicas. This is a + pointer to distinguish between explicit zero and unspecified. Defaults + to 1. + format: int32 + type: integer + selector: + description: 'Selector is a label query over machines that should + match the replica count. Label keys and values that must match in + order to be controlled by this MachineSet. It must match the machine + template''s labels. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors' + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + template: + description: Template is the object that describes the machine that + will be created if insufficient replicas are detected. + properties: + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + description: 'Annotations is an unstructured key value map + stored with a resource that may be set by external tools + to store and retrieve arbitrary metadata. They are not queryable + and should be preserved when modifying objects. More info: + http://kubernetes.io/docs/user-guide/annotations' + type: object + generateName: + description: "GenerateName is an optional prefix, used by + the server, to generate a unique name ONLY IF the Name field + has not been provided. If this field is used, the name returned + to the client will be different than the name passed. This + value will also be combined with a unique suffix. The provided + value has the same validation rules as the Name field, and + may be truncated by the length of the suffix required to + make the value unique on the server. \n If this field is + specified and the generated name exists, the server will + NOT return a 409 - instead, it will either return 201 Created + or 500 with Reason ServerTimeout indicating a unique name + could not be found in the time allotted, and the client + should retry (optionally after the time indicated in the + Retry-After header). \n Applied only if Name is not specified. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency" + type: string + labels: + additionalProperties: + type: string + description: 'Map of string keys and values that can be used + to organize and categorize (scope and select) objects. May + match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels' + type: object + name: + description: 'Name must be unique within a namespace. Is required + when creating resources, although some resources may allow + a client to request the generation of an appropriate name + automatically. Name is primarily intended for creation idempotence + and configuration definition. Cannot be updated. More info: + http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + namespace: + description: "Namespace defines the space within each name + must be unique. An empty namespace is equivalent to the + \"default\" namespace, but \"default\" is the canonical + representation. Not all objects are required to be scoped + to a namespace - the value of this field for those objects + will be empty. \n Must be a DNS_LABEL. Cannot be updated. + More info: http://kubernetes.io/docs/user-guide/namespaces" + type: string + ownerReferences: + description: List of objects depended by this object. If ALL + objects in the list have been deleted, this object will + be garbage collected. If this object is managed by a controller, + then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more + than one managing controller. + items: + description: OwnerReference contains enough information + to let you identify an owning object. An owning object + must be in the same namespace as the dependent, or be + cluster-scoped, so there is no namespace field. + properties: + apiVersion: + description: API version of the referent. + type: string + blockOwnerDeletion: + description: If true, AND if the owner has the "foregroundDeletion" + finalizer, then the owner cannot be deleted from the + key-value store until this reference is removed. See + https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion + for how the garbage collector interacts with this + field and enforces the foreground deletion. Defaults + to false. To set this field, a user needs "delete" + permission of the owner, otherwise 422 (Unprocessable + Entity) will be returned. + type: boolean + controller: + description: If true, this reference points to the managing + controller. + type: boolean + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids' + type: string + required: + - apiVersion + - kind + - name + - uid + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - uid + x-kubernetes-list-type: map + type: object + spec: + description: 'Specification of the desired behavior of the machine. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + lifecycleHooks: + description: LifecycleHooks allow users to pause operations + on the machine at certain predefined points within the machine + lifecycle. + properties: + preDrain: + description: PreDrain hooks prevent the machine from being + drained. This also blocks further lifecycle events, + such as termination. + items: + description: LifecycleHook represents a single instance + of a lifecycle hook + properties: + name: + description: Name defines a unique name for the + lifcycle hook. The name should be unique and descriptive, + ideally 1-3 words, in CamelCase or it may be namespaced, + eg. foo.example.com/CamelCase. Names must be unique + and should only be managed by a single entity. + maxLength: 256 + minLength: 3 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + owner: + description: Owner defines the owner of the lifecycle + hook. This should be descriptive enough so that + users can identify who/what is responsible for + blocking the lifecycle. This could be the name + of a controller (e.g. clusteroperator/etcd) or + an administrator managing the hook. + maxLength: 512 + minLength: 3 + type: string + required: + - name + - owner + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + preTerminate: + description: PreTerminate hooks prevent the machine from + being terminated. PreTerminate hooks be actioned after + the Machine has been drained. + items: + description: LifecycleHook represents a single instance + of a lifecycle hook + properties: + name: + description: Name defines a unique name for the + lifcycle hook. The name should be unique and descriptive, + ideally 1-3 words, in CamelCase or it may be namespaced, + eg. foo.example.com/CamelCase. Names must be unique + and should only be managed by a single entity. + maxLength: 256 + minLength: 3 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + owner: + description: Owner defines the owner of the lifecycle + hook. This should be descriptive enough so that + users can identify who/what is responsible for + blocking the lifecycle. This could be the name + of a controller (e.g. clusteroperator/etcd) or + an administrator managing the hook. + maxLength: 512 + minLength: 3 + type: string + required: + - name + - owner + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + metadata: + description: ObjectMeta will autopopulate the Node created. + Use this to indicate what labels, annotations, name prefix, + etc., should be used when creating the Node. + properties: + annotations: + additionalProperties: + type: string + description: 'Annotations is an unstructured key value + map stored with a resource that may be set by external + tools to store and retrieve arbitrary metadata. They + are not queryable and should be preserved when modifying + objects. More info: http://kubernetes.io/docs/user-guide/annotations' + type: object + generateName: + description: "GenerateName is an optional prefix, used + by the server, to generate a unique name ONLY IF the + Name field has not been provided. If this field is used, + the name returned to the client will be different than + the name passed. This value will also be combined with + a unique suffix. The provided value has the same validation + rules as the Name field, and may be truncated by the + length of the suffix required to make the value unique + on the server. \n If this field is specified and the + generated name exists, the server will NOT return a + 409 - instead, it will either return 201 Created or + 500 with Reason ServerTimeout indicating a unique name + could not be found in the time allotted, and the client + should retry (optionally after the time indicated in + the Retry-After header). \n Applied only if Name is + not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency" + type: string + labels: + additionalProperties: + type: string + description: 'Map of string keys and values that can be + used to organize and categorize (scope and select) objects. + May match selectors of replication controllers and services. + More info: http://kubernetes.io/docs/user-guide/labels' + type: object + name: + description: 'Name must be unique within a namespace. + Is required when creating resources, although some resources + may allow a client to request the generation of an appropriate + name automatically. Name is primarily intended for creation + idempotence and configuration definition. Cannot be + updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names' + type: string + namespace: + description: "Namespace defines the space within each + name must be unique. An empty namespace is equivalent + to the \"default\" namespace, but \"default\" is the + canonical representation. Not all objects are required + to be scoped to a namespace - the value of this field + for those objects will be empty. \n Must be a DNS_LABEL. + Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces" + type: string + ownerReferences: + description: List of objects depended by this object. + If ALL objects in the list have been deleted, this object + will be garbage collected. If this object is managed + by a controller, then an entry in this list will point + to this controller, with the controller field set to + true. There cannot be more than one managing controller. + items: + description: OwnerReference contains enough information + to let you identify an owning object. An owning object + must be in the same namespace as the dependent, or + be cluster-scoped, so there is no namespace field. + properties: + apiVersion: + description: API version of the referent. + type: string + blockOwnerDeletion: + description: If true, AND if the owner has the "foregroundDeletion" + finalizer, then the owner cannot be deleted from + the key-value store until this reference is removed. + See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion + for how the garbage collector interacts with this + field and enforces the foreground deletion. Defaults + to false. To set this field, a user needs "delete" + permission of the owner, otherwise 422 (Unprocessable + Entity) will be returned. + type: boolean + controller: + description: If true, this reference points to the + managing controller. + type: boolean + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids' + type: string + required: + - apiVersion + - kind + - name + - uid + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - uid + x-kubernetes-list-type: map + type: object + providerID: + description: ProviderID is the identification ID of the machine + provided by the provider. This field must match the provider + ID as seen on the node object corresponding to this machine. + This field is required by higher level consumers of cluster-api. + Example use case is cluster autoscaler with cluster-api + as provider. Clean-up logic in the autoscaler compares machines + to nodes to find out machines at provider which could not + get registered as Kubernetes nodes. With cluster-api as + a generic out-of-tree provider for autoscaler, this field + is required by autoscaler to be able to have a provider + view of the list of machines. Another list of nodes is queried + from the k8s apiserver and then a comparison is done to + find out unregistered machines and are marked for delete. + This field will be set by the actuators and consumed by + higher level entities like autoscaler that will be interfacing + with cluster-api as generic provider. + type: string + providerSpec: + description: ProviderSpec details Provider-specific configuration + to use during node creation. + properties: + value: + description: Value is an inlined, serialized representation + of the resource configuration. It is recommended that + providers maintain their own versioned API types that + should be serialized/deserialized from this field, akin + to component config. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + taints: + description: The list of the taints to be applied to the corresponding + Node in additive manner. This list will not overwrite any + other taints added to the Node on an ongoing basis by other + entities. These taints should be actively reconciled e.g. + if you ask the machine controller to apply a taint and then + manually remove the taint the machine controller will put + it back) but not have the machine controller remove any + taints + items: + description: The node this Taint is attached to has the + "effect" on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods + that do not tolerate the taint. Valid effects are + NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Required. The taint key to be applied to + a node. + type: string + timeAdded: + description: TimeAdded represents the time at which + the taint was added. It is only written for NoExecute + taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint + key. + type: string + required: + - effect + - key + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + type: object + status: + description: MachineSetStatus defines the observed state of MachineSet + properties: + availableReplicas: + description: The number of available replicas (ready for at least + minReadySeconds) for this MachineSet. + format: int32 + type: integer + conditions: + description: Conditions defines the current state of the MachineSet + items: + description: Condition defines an observation of a Machine API resource + operational state. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. This should be when the underlying condition changed. + If that is not known, then using the time when the API field + changed is acceptable. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. This field may be empty. + type: string + reason: + description: The reason for the condition's last transition + in CamelCase. The specific API may choose whether or not this + field is considered a guaranteed API. This field may not be + empty. + type: string + severity: + description: Severity provides an explicit classification of + Reason code, so the users or machines can immediately understand + the current situation and act accordingly. The Severity field + MUST be set only when Status=False. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. + type: string + required: + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + errorMessage: + type: string + errorReason: + description: "In the event that there is a terminal problem reconciling + the replicas, both ErrorReason and ErrorMessage will be set. ErrorReason + will be populated with a succinct value suitable for machine interpretation, + while ErrorMessage will contain a more verbose string suitable for + logging and human consumption. \n These fields should not be set + for transitive errors that a controller faces that are expected + to be fixed automatically over time (like service outages), but + instead indicate that something is fundamentally wrong with the + MachineTemplate's spec or the configuration of the machine controller, + and that manual intervention is required. Examples of terminal errors + would be invalid combinations of settings in the spec, values that + are unsupported by the machine controller, or the responsible machine + controller itself being critically misconfigured. \n Any transient + errors that occur during the reconciliation of Machines can be added + as events to the MachineSet object and/or logged in the controller's + output." + type: string + fullyLabeledReplicas: + description: The number of replicas that have labels matching the + labels of the machine template of the MachineSet. + format: int32 + type: integer + observedGeneration: + description: ObservedGeneration reflects the generation of the most + recently observed MachineSet. + format: int64 + type: integer + readyReplicas: + description: The number of ready replicas for this MachineSet. A machine + is considered ready when the node has been created and is "Ready". + format: int32 + type: integer + replicas: + description: Replicas is the most recently observed number of replicas. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.labelSelector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} diff --git a/config/samples/machine-cloudscale-generic.yml b/config/samples/machine-cloudscale-generic.yml new file mode 100644 index 0000000..c2db3d7 --- /dev/null +++ b/config/samples/machine-cloudscale-generic.yml @@ -0,0 +1,26 @@ +# Minimal generic config that will not join any node but just creating a machine +apiVersion: machine.openshift.io/v1beta1 +kind: Machine +metadata: + annotations: {} + generateName: app- + labels: + machine.openshift.io/cluster-api-cluster: cluster-1 +spec: + lifecycleHooks: {} + metadata: + labels: + node-role.kubernetes.io/app: "" + node-role.kubernetes.io/worker: "" + providerSpec: + value: + zone: lpg1 + baseDomain: cluster-1.appuio.io + flavor: flex-4-1 + image: debian-12 + rootVolumeSizeGB: 50 + antiAffinityKey: app + sshKeys: [] + interfaces: + - type: Public +status: {} diff --git a/config/samples/machine-cloudscale-known-working.yml b/config/samples/machine-cloudscale-known-working.yml new file mode 100644 index 0000000..34aba53 --- /dev/null +++ b/config/samples/machine-cloudscale-known-working.yml @@ -0,0 +1,40 @@ +# Known working config creating a node that will join a OCP cluster. +# Create the userDataSecret with +# $ ./pkg/machine/userdata/userdata-secret-from-maintfjson.sh c-appuio-lab-cloudscale-rma-0/manifests/openshift4-terraform/main.tf.json +# +# Created from this config: +# $ k get machine,node app-7ws9q +# NAME PHASE TYPE REGION ZONE AGE +# machine.machine.openshift.io/app-7ws9q Running flex-16-4 rma rma1 3m38s +# +# NAME STATUS ROLES AGE VERSION +# node/app-7ws9q Ready app,worker 59s v1.28.13+2ca1a23 +apiVersion: machine.openshift.io/v1beta1 +kind: Machine +metadata: + annotations: {} + generateName: app- + labels: + machine.openshift.io/cluster-api-cluster: c-appuio-lab-cloudscale-rma-0 +spec: + lifecycleHooks: {} + metadata: + labels: + node-role.kubernetes.io/app: "" + node-role.kubernetes.io/worker: "" + providerSpec: + value: + zone: rma1 + baseDomain: lab-cloudscale-rma-0.appuio.cloud + flavor: flex-16-4 + image: custom:rhcos-4.15 + rootVolumeSizeGB: 100 + antiAffinityKey: app + interfaces: + - type: Private + networkUUID: fd2b132d-f5d0-4024-b99f-68e5321ab4d1 + userDataSecret: + name: cloudscale-user-data + tokenSecret: + name: cloudscale-rw-token +status: {} diff --git a/controllers/machineset_controller.go b/controllers/machineset_controller.go new file mode 100644 index 0000000..dbeba3f --- /dev/null +++ b/controllers/machineset_controller.go @@ -0,0 +1,150 @@ +package controllers + +import ( + "context" + "fmt" + "regexp" + "slices" + "strconv" + "strings" + + machinev1beta1 "github.com/openshift/api/machine/v1beta1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + csv1beta1 "github.com/appuio/machine-api-provider-cloudscale/api/cloudscale/provider/v1beta1" +) + +// MachineSetReconciler reconciles a MachineSet object +type MachineSetReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +const ( + // This exposes compute information based on the providerSpec input. + // This is needed by the autoscaler to foresee upcoming capacity when scaling from zero. + // https://github.com/openshift/enhancements/pull/186 + cpuKey = "machine.openshift.io/vCPU" + memoryKey = "machine.openshift.io/memoryMb" + gpuKey = "machine.openshift.io/GPU" + labelsKey = "capacity.cluster-autoscaler.kubernetes.io/labels" + + gpuKeyValue = "0" + arch = "kubernetes.io/arch=amd64" +) + +// Reconcile reacts to MachineSet changes and updates the annotations used by the OpenShift autoscaler. +// GPU is always set to 0, as cloudscale does not provide GPU instances. +// The architecture label is always set to amd64. +func (r *MachineSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var machineSet machinev1beta1.MachineSet + if err := r.Get(ctx, req.NamespacedName, &machineSet); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + if !machineSet.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + origSet := machineSet.DeepCopy() + + if machineSet.Annotations == nil { + machineSet.Annotations = make(map[string]string) + } + + spec, err := csv1beta1.ProviderSpecFromRawExtension(machineSet.Spec.Template.Spec.ProviderSpec.Value) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get provider spec from machine template: %w", err) + } + if spec == nil { + return ctrl.Result{}, nil + } + + flavor, err := parseCloudscaleFlavor(spec.Flavor) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to parse flavor %q: %w", spec.Flavor, err) + } + + machineSet.Annotations[cpuKey] = strconv.Itoa(flavor.CPU) + machineSet.Annotations[memoryKey] = strconv.Itoa(flavor.MemGB * 1024) + machineSet.Annotations[gpuKey] = gpuKeyValue + + // We guarantee that any existing labels provided via the capacity annotations are preserved. + // See https://github.com/kubernetes/autoscaler/pull/5382 and https://github.com/kubernetes/autoscaler/pull/5697 + machineSet.Annotations[labelsKey] = mergeCommaSeparatedKeyValuePairs( + arch, + machineSet.Annotations[labelsKey]) + + if equality.Semantic.DeepEqual(origSet.Annotations, machineSet.Annotations) { + return ctrl.Result{}, nil + } + + if err := r.Patch(ctx, &machineSet, client.MergeFrom(origSet)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to patch MachineSet %q: %w", machineSet.Name, err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MachineSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&machinev1beta1.MachineSet{}). + Complete(r) +} + +type cloudscaleFlavor struct { + Type string + CPU int + MemGB int +} + +var cloudscaleFlavorRegexp = regexp.MustCompile(`^(\w+)-(\d+)-(\d+)$`) + +// Parse parses a cloudscale flavor string. +func parseCloudscaleFlavor(flavor string) (cloudscaleFlavor, error) { + parts := cloudscaleFlavorRegexp.FindStringSubmatch(flavor) + + if len(parts) != 4 { + return cloudscaleFlavor{}, fmt.Errorf("flavor %q does not match expected format", flavor) + } + mem, err := strconv.Atoi(parts[2]) + if err != nil { + return cloudscaleFlavor{}, fmt.Errorf("failed to parse memory from flavor %q: %w", flavor, err) + } + cpu, err := strconv.Atoi(parts[3]) + if err != nil { + return cloudscaleFlavor{}, fmt.Errorf("failed to parse CPU from flavor %q: %w", flavor, err) + } + + return cloudscaleFlavor{ + Type: parts[1], + CPU: cpu, + MemGB: mem, + }, nil +} + +// mergeCommaSeparatedKeyValuePairs merges multiple comma separated lists of key=value pairs into a single, comma-separated, list +// of key=value pairs. If a key is present in multiple lists, the value from the last list is used. +func mergeCommaSeparatedKeyValuePairs(lists ...string) string { + merged := make(map[string]string) + for _, list := range lists { + for _, kv := range strings.Split(list, ",") { + kv := strings.Split(kv, "=") + if len(kv) != 2 { + // ignore invalid key=value pairs + continue + } + merged[kv[0]] = kv[1] + } + } + // convert the map back to a comma separated list + var result []string + for k, v := range merged { + result = append(result, fmt.Sprintf("%s=%s", k, v)) + } + slices.Sort(result) + return strings.Join(result, ",") +} diff --git a/controllers/machineset_controller_test.go b/controllers/machineset_controller_test.go new file mode 100644 index 0000000..6633a2b --- /dev/null +++ b/controllers/machineset_controller_test.go @@ -0,0 +1,64 @@ +package controllers + +import ( + "context" + "fmt" + "testing" + + machinev1beta1 "github.com/openshift/api/machine/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func Test_MachineSetReconciler_Reconcile(t *testing.T) { + ctx := context.Background() + + scheme := runtime.NewScheme() + require.NoError(t, clientgoscheme.AddToScheme(scheme)) + require.NoError(t, machinev1beta1.AddToScheme(scheme)) + + ms := &machinev1beta1.MachineSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "machineset1", + Namespace: "default", + Annotations: map[string]string{ + "random": "annotation", + labelsKey: "a=a,b=b", + }, + }, + Spec: machinev1beta1.MachineSetSpec{}, + } + + setFlavorOnMachineSet(ms, "plus-4-2") + + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithRuntimeObjects(ms). + Build() + + subject := &MachineSetReconciler{ + Client: c, + Scheme: scheme, + } + + _, err := subject.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(ms)}) + require.NoError(t, err) + updated := &machinev1beta1.MachineSet{} + require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(ms), updated)) + assert.Equal(t, "2", updated.Annotations[cpuKey]) + assert.Equal(t, "4096", updated.Annotations[memoryKey]) + assert.Equal(t, "0", updated.Annotations[gpuKey]) + assert.Equal(t, "a=a,b=b,kubernetes.io/arch=amd64", updated.Annotations[labelsKey]) +} + +func setFlavorOnMachineSet(machine *machinev1beta1.MachineSet, flavor string) { + machine.Spec.Template.Spec.ProviderSpec.Value = &runtime.RawExtension{ + Raw: []byte(fmt.Sprintf(`{"flavor": "%s"}`, flavor)), + } +} diff --git a/go.mod b/go.mod index 6c4b9ed..5e0b7f8 100644 --- a/go.mod +++ b/go.mod @@ -5,69 +5,113 @@ go 1.23 toolchain go1.23.2 require ( + github.com/cloudscale-ch/cloudscale-go-sdk/v5 v5.0.1 + github.com/google/go-jsonnet v0.20.0 + github.com/openshift/api v0.0.0-20240924155631-232984653385 + github.com/openshift/library-go v0.0.0-20240919205913-c96b82b3762b + github.com/openshift/machine-api-operator v0.2.1-0.20241009125928-52a965a42fac + github.com/stretchr/testify v1.9.0 + go.uber.org/mock v0.5.0 + k8s.io/api v0.31.1 k8s.io/apimachinery v0.31.1 + k8s.io/apiserver v0.31.1 k8s.io/client-go v0.31.1 + k8s.io/component-base v0.31.1 + k8s.io/klog/v2 v2.130.1 + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 sigs.k8s.io/controller-runtime v0.19.0 + sigs.k8s.io/controller-tools v0.16.4 + sigs.k8s.io/yaml v1.4.0 ) require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.10 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.20.1 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/onsi/gomega v1.34.2 // indirect + github.com/openshift/client-go v0.0.0-20240918182115-6a8ead8397fd // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.31.1 // indirect k8s.io/apiextensions-apiserver v0.31.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/cli-runtime v0.31.1 // indirect k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect - k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect - sigs.k8s.io/json v0.0.0-20241009153224-e386a8af8d30 // indirect + k8s.io/kubectl v0.31.1 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kustomize/api v0.18.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index bb1d8d9..76dad51 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,122 @@ +4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= +4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= +4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= +4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= +github.com/4meepo/tagalign v1.3.4 h1:P51VcvBnf04YkHzjfclN6BbsopfJR5rxs1n+5zHt+w8= +github.com/4meepo/tagalign v1.3.4/go.mod h1:M+pnkHH2vG8+qhE5bVc/zeP7HS/j910Fwa9TUSyZVI0= +github.com/Abirdcfly/dupword v0.1.1 h1:Bsxe0fIw6OwBtXMIncaTxCLHYO5BB+3mcsR5E8VXloY= +github.com/Abirdcfly/dupword v0.1.1/go.mod h1:B49AcJdTYYkpd4HjgAcutNGG9HZ2JWwKunH9Y2BA6sM= +github.com/Antonboom/errname v0.1.13 h1:JHICqsewj/fNckzrfVSe+T33svwQxmjC+1ntDsHOVvM= +github.com/Antonboom/errname v0.1.13/go.mod h1:uWyefRYRN54lBg6HseYCFhs6Qjcy41Y3Jl/dVhA87Ns= +github.com/Antonboom/nilnil v0.1.9 h1:eKFMejSxPSA9eLSensFmjW2XTgTwJMjZ8hUHtV4s/SQ= +github.com/Antonboom/nilnil v0.1.9/go.mod h1:iGe2rYwCq5/Me1khrysB4nwI7swQvjclR8/YRPl5ihQ= +github.com/Antonboom/testifylint v1.4.3 h1:ohMt6AHuHgttaQ1xb6SSnxCeK4/rnK7KKzbvs7DmEck= +github.com/Antonboom/testifylint v1.4.3/go.mod h1:+8Q9+AOLsz5ZiQiiYujJKs9mNz398+M6UgslP4qgJLA= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Crocmagnon/fatcontext v0.5.2 h1:vhSEg8Gqng8awhPju2w7MKHqMlg4/NI+gSDHtR3xgwA= +github.com/Crocmagnon/fatcontext v0.5.2/go.mod h1:87XhRMaInHP44Q7Tlc7jkgKKB7kZAOPiDkFMdKCC+74= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC23cV0kEsN1MVMNqeOW43cU= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= +github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= +github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= +github.com/alecthomas/go-check-sumtype v0.1.4/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alexkohler/nakedret/v2 v2.0.4 h1:yZuKmjqGi0pSmjGpOC016LtPJysIL0WEUiaXW5SUnNg= +github.com/alexkohler/nakedret/v2 v2.0.4/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= +github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= +github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v4 v4.4.1 h1:jfUaCkN+aUpobrMO24zwyAMwMAV5eSziCkOKEauOLdw= +github.com/bombsimon/wsl/v4 v4.4.1/go.mod h1:Xu/kDxGZTofQcDGCtQe9KCzhHphIe0fDuyWTxER9Feo= +github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= +github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= +github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= +github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/butuzov/ireturn v0.3.0 h1:hTjMqWw3y5JC3kpnC5vXmFJAWI/m31jaCYQqzkS6PL0= +github.com/butuzov/ireturn v0.3.0/go.mod h1:A09nIiwiqzN/IoVo9ogpa0Hzi9fex1kd9PSD6edP5ZA= +github.com/butuzov/mirror v1.2.0 h1:9YVK1qIjNspaqWutSv8gsge2e/Xpq1eqEkslEUHy5cs= +github.com/butuzov/mirror v1.2.0/go.mod h1:DqZZDtzm42wIAIyHXeN8W/qb1EPlb9Qn/if9icBOpdQ= +github.com/catenacyber/perfsprint v0.7.1 h1:PGW5G/Kxn+YrN04cRAZKC+ZuvlVwolYMrIyyTJ/rMmc= +github.com/catenacyber/perfsprint v0.7.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/ckaznocha/intrange v0.2.0 h1:FykcZuJ8BD7oX93YbO1UY9oZtkRbp+1/kJcDjkefYLs= +github.com/ckaznocha/intrange v0.2.0/go.mod h1:r5I7nUlAAG56xmkOpw4XVr16BXhwYTUdcuRFeevn1oE= +github.com/cloudscale-ch/cloudscale-go-sdk/v5 v5.0.1 h1:itMZkis7IXnZblCL58ClG21QIb3FmGqYMCIcihUhvso= +github.com/cloudscale-ch/cloudscale-go-sdk/v5 v5.0.1/go.mod h1:6m7Ct04hW3BfiPE42e+sggWqiXGtZMOgGCOGolBtMLo= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= +github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= +github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= +github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/ghostiam/protogetter v0.3.6 h1:R7qEWaSgFCsy20yYHNIJsU9ZOb8TziSRRxuAOTVKeOk= +github.com/ghostiam/protogetter v0.3.6/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= +github.com/go-critic/go-critic v0.11.4 h1:O7kGOCx0NDIni4czrkRIXTnit0mkyKOCePh3My6OyEU= +github.com/go-critic/go-critic v0.11.4/go.mod h1:2QAdo4iuLik5S9YG0rT4wcZ8QxwHYkrr6/2MWAiv/vc= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -28,58 +129,229 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9 h1:/1322Qns6BtQxUZDTAT4SdcoxknUki7IAoK4SAXr8ME= +github.com/golangci/gofmt v0.0.0-20240816233607-d8596aa466a9/go.mod h1:Oesb/0uFAyWoaw1U1qS5zyjCg5NP9C9iwjnI4tIsXEE= +github.com/golangci/golangci-lint v1.61.0 h1:VvbOLaRVWmyxCnUIMTbf1kDsaJbTzH20FAMXTAlQGu8= +github.com/golangci/golangci-lint v1.61.0/go.mod h1:e4lztIrJJgLPhWvFPDkhiMwEFRrWlmFbrZea3FsJyN8= +github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= +github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/modinfo v0.3.4 h1:oU5huX3fbxqQXdfspamej74DFX0kyGLkw1ppvXoJ8GA= +github.com/golangci/modinfo v0.3.4/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= +github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/revgrep v0.5.3 h1:3tL7c1XBMtWHHqVpS5ChmiAAoe4PF/d5+ULzV9sLAzs= +github.com/golangci/revgrep v0.5.3/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= +github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= +github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= +github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jjti/go-spancheck v0.6.2 h1:iYtoxqPMzHUPp7St+5yA8+cONdyXD3ug6KK15n7Pklk= +github.com/jjti/go-spancheck v0.6.2/go.mod h1:+X7lvIrR5ZdUTkxFYqzJ0abr8Sb5LOo80uOhWNqIrYA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= +github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= +github.com/karamaru-alpha/copyloopvar v1.1.0 h1:x7gNyKcC2vRBO1H2Mks5u1VxQtYvFiym7fCjIP8RPos= +github.com/karamaru-alpha/copyloopvar v1.1.0/go.mod h1:u7CIfztblY0jZLOQZgH3oYsJzpC2A7S6u/lfgSXHy0k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.7.0 h1:+SbscKmWJ5mOK/bO1zS60F5I9WwZDWOfRsC4RwfwRV0= +github.com/kisielk/errcheck v1.7.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= +github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= +github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= +github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= +github.com/lasiar/canonicalheader v1.1.1 h1:wC+dY9ZfiqiPwAexUApFush/csSPXeIi4QqyxXmng8I= +github.com/lasiar/canonicalheader v1.1.1/go.mod h1:cXkb3Dlk6XXy+8MVQnF23CYKWlyA7kfQhSw2CcZtZb0= +github.com/ldez/gomoddirectives v0.2.4 h1:j3YjBIjEBbqZ0NKtBNzr8rtMHTOrLPeiwTkfUJZ3alg= +github.com/ldez/gomoddirectives v0.2.4/go.mod h1:oWu9i62VcQDYp9EQ0ONTfqLNh+mDLWWDO+SO0qSQw5g= +github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= +github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= +github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= +github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgechev/revive v1.3.9 h1:18Y3R4a2USSBF+QZKFQwVkBROUda7uoBlkEuBD+YD1A= +github.com/mgechev/revive v1.3.9/go.mod h1:+uxEIr5UH0TjXWHTno3xh4u7eg6jDpXKzQccA9UGhHU= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.16.2 h1:8iLqHIZvN4fTLDC0Ke9tbSZVcyVHoBs0HIbnVSxfHJk= +github.com/nunnatsa/ginkgolinter v0.16.2/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/openshift/api v0.0.0-20240924155631-232984653385 h1:P6O191HwBj0ahEfea2wkvxhmW2fzXhvCh8hwHDvUozM= +github.com/openshift/api v0.0.0-20240924155631-232984653385/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM= +github.com/openshift/client-go v0.0.0-20240918182115-6a8ead8397fd h1:Gd0+bYdcfGIsDOJ8BwTJJjQeXoziyIsTwqp/s38rKyM= +github.com/openshift/client-go v0.0.0-20240918182115-6a8ead8397fd/go.mod h1:EB7GeA/vpf9AHklMgnnT0+uG6l/3f8cChtCFbJFrk4g= +github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20241007145816-7038c320d36c h1:9A/0QoTZo2xh5j6nmh5CGNVBG8Ql1RmXmCcrikBnG+w= +github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20241007145816-7038c320d36c/go.mod h1:EN1Sv7kcVtaLUiXpZ8V0iSiJxNPPz1H3ZhCmNRpJWZM= +github.com/openshift/cluster-control-plane-machine-set-operator v0.0.0-20240909043600-373ac49835bf h1:mfMmaD9+vZIZQq3MGXsS/AGHXekj4wIn3zc1Cs1EY8M= +github.com/openshift/cluster-control-plane-machine-set-operator v0.0.0-20240909043600-373ac49835bf/go.mod h1:2fZsjZ3QSPkoMUc8QntXfeBb8AnvW+WIYwwQX8vmgvQ= +github.com/openshift/library-go v0.0.0-20240919205913-c96b82b3762b h1:y2DduJug7UZqTu0QTkRPAu73nskuUbFA66fmgxVf/fI= +github.com/openshift/library-go v0.0.0-20240919205913-c96b82b3762b/go.mod h1:f8QcnrooSwGa96xI4UaKbKGJZskhTCGeimXKyc4t/ZU= +github.com/openshift/machine-api-operator v0.2.1-0.20241009125928-52a965a42fac h1:qlOVMOPOJ/0YRDKUk5FUZspJSILUo/7P3V5Kd8vP4d8= +github.com/openshift/machine-api-operator v0.2.1-0.20241009125928-52a965a42fac/go.mod h1:gXjplVdC80eclLdaZbWsDbbewhHmE99CVk4o61TiTbM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyfloyd/go-errorlint v1.6.0 h1:tftWV9DE7txiFzPpztTAwyoRLKNj9gpVm2cg8/OwcYY= +github.com/polyfloyd/go-errorlint v1.6.0/go.mod h1:HR7u8wuP1kb1NeN1zqTd1ZMlqUKPPHF+Id4vIPvDqVw= github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -88,20 +360,129 @@ github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJN github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= +github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.27.0 h1:t/3jZpSXtRPRf2xr0m63i32ZrusyurIGT9E5wAvXQnI= +github.com/sashamelentyev/usestdlibvars v1.27.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/securego/gosec/v2 v2.21.2 h1:deZp5zmYf3TWwU7A7cR2+SolbTpZ3HQiwFqnzQyEl3M= +github.com/securego/gosec/v2 v2.21.2/go.mod h1:au33kg78rNseF5PwPnTWhuYBFf534bvJRvOrgZ/bFzU= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/tenv v1.10.0 h1:g/hzMA+dBCKqGXgW8AV/1xIWhAvDrx0zFKNR48NFMg0= +github.com/sivchari/tenv v1.10.0/go.mod h1:tdY24masnVoZFxYrHv/nD6Tc8FbkEtAQEEziXpyMgqY= +github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= +github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= +github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= +github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= +github.com/tetafro/godot v1.4.17 h1:pGzu+Ye7ZUEFx7LHU0dAKmCOXWsPjl7qA6iMGndsjPs= +github.com/tetafro/godot v1.4.17/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= +github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= +github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= +github.com/tomarrell/wrapcheck/v2 v2.9.0 h1:801U2YCAjLhdN8zhZ/7tdjB3EnAoRlJHt/s+9hijLQ4= +github.com/tomarrell/wrapcheck/v2 v2.9.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= +github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= +github.com/ultraware/whitespace v0.1.1 h1:bTPOGejYFulW3PkcrqkeQwOd6NKOOXvmGD9bo/Gk8VQ= +github.com/ultraware/whitespace v0.1.1/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/uudashr/gocognit v1.1.3 h1:l+a111VcDbKfynh+airAy/DJQKaXh2m9vkoysMPSZyM= +github.com/uudashr/gocognit v1.1.3/go.mod h1:aKH8/e8xbTRBwjbCkwZ8qt4l2EpKXl31KMHgSS+lZ2U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= +go-simpler.org/musttag v0.12.2 h1:J7lRc2ysXOq7eM8rwaTYnNrHd5JwjppzB6mScysB2Cs= +go-simpler.org/musttag v0.12.2/go.mod h1:uN1DVIasMTQKk6XSik7yrJoEysGtR2GRqvWnI9S7TYM= +go-simpler.org/sloglint v0.7.2 h1:Wc9Em/Zeuu7JYpl+oKoYOsQSy2X560aVueCW/m6IijY= +go-simpler.org/sloglint v0.7.2/go.mod h1:US+9C80ppl7VsThQclkM7BkCHQAzuz8kHLsW3ppuluo= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -109,18 +490,18 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= +golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= @@ -128,42 +509,36 @@ golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= -golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -173,33 +548,56 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= +k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= +k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk= +k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U= k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= +k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= -k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k= k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094/go.mod h1:7ioBJr1A6igWjsR2fxq2EZ0mlMwYLejazSIc2bzMp2U= +k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= +k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/json v0.0.0-20241009153224-e386a8af8d30 h1:ObU1vgTtAle8WwCKgcDkPjLJYwlazQpIjzSA0asMhy4= -sigs.k8s.io/json v0.0.0-20241009153224-e386a8af8d30/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20240923090159-236e448db12c h1:w1vANkdIpYwbEZH0y1C7iJItgdEGvF9A3eCdRmLhg8I= +sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20240923090159-236e448db12c/go.mod h1:IaDsO8xSPRxRG1/rm9CP7+jPmj0nMNAuNi/yiHnLX8k= +sigs.k8s.io/controller-tools v0.16.4 h1:VXmar78eDXbx1by/H09ikEq1hiq3bqInxuV3lMr3GmQ= +sigs.k8s.io/controller-tools v0.16.4/go.mod h1:kcsZyYMXiOFuBhofSPtkB90zTSxVRxVVyvtKQcx3q1A= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= +sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= +sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= diff --git a/hack/deploy-nodelink-controller.sh b/hack/deploy-nodelink-controller.sh new file mode 100755 index 0000000..18a4218 --- /dev/null +++ b/hack/deploy-nodelink-controller.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# This script deploys the nodelink controller as dev-nodelink-controller with the same image as the upstream machine-api-operator. +# This allows testing this provider on a cluster that does not yet have a full machine-api-controllers deployment. +# If the machine-api-controllers deployment is already present, this script will skip the deployment. +set -euo pipefail + + +UPSTREAM_NODELINK_DEPLOYMENT="machine-api-controllers" +UPSTREAM_MACHINE_API_OPERATOR_DEPLOYMENT="machine-api-operator" + +if kubectl get deployment "${UPSTREAM_NODELINK_DEPLOYMENT}" &> /dev/null; then + echo "Real upstream nodelink deployment already exists, skipping" + exit 0 +fi + +tmpdir=$(mktemp -d) + +image=$(kubectl get deployment "${UPSTREAM_MACHINE_API_OPERATOR_DEPLOYMENT}" -oyaml | yq '.spec.template.spec.containers | filter(.name == "machine-api-operator") | .[0].image') + +imageParts=(${image//@/ }) + +echo "Deploying nodelink as 'dev-nodelink-controller' with image '${imageParts[0]}@${imageParts[1]}'" + +cp hack/nodelink-controller.yaml "${tmpdir}/nodelink-deployment.yaml" + +cat > "${tmpdir}/Kustomization.yaml" << YAML +resources: +- nodelink-deployment.yaml + +images: +- name: ${imageParts[0]} + digest: ${imageParts[1]} +YAML + +kustomize build "${tmpdir}" | kubectl apply -f - + +rm -rf "${tmpdir}" diff --git a/hack/nodelink-controller.yaml b/hack/nodelink-controller.yaml new file mode 100644 index 0000000..26949e3 --- /dev/null +++ b/hack/nodelink-controller.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dev-nodelink-controller + namespace: openshift-machine-api +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: dev-nodelink-controller + template: + metadata: + labels: + app: dev-nodelink-controller + spec: + containers: + - args: + - --logtostderr=true + - --v=3 + - --leader-elect=true + - --leader-elect-lease-duration=120s + - --namespace=openshift-machine-api + command: + - /nodelink-controller + image: quay.io/openshift-release-dev/ocp-v4.0-art-dev + imagePullPolicy: IfNotPresent + name: nodelink-controller + resources: + requests: + cpu: 10m + memory: 20Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + restartPolicy: Always + serviceAccountName: machine-api-controllers +status: {} diff --git a/hack/sync-crds.sh b/hack/sync-crds.sh new file mode 100755 index 0000000..872c866 --- /dev/null +++ b/hack/sync-crds.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +# map names of CRD files between the vendored openshift/api repository and the ./install directory +CRDS_MAPPING=( "0000_10_machine-api_01_machines-Default.crd.yaml:machine.openshift.io.crd.yaml" + "0000_10_machine-api_01_machinesets-Default.crd.yaml:machineset.openshift.io.crd.yaml" + "0000_10_machine-api_01_machinehealthchecks.crd.yaml:machinehealthcheck.openshift.io.crd.yaml" ) + +for crd in "${CRDS_MAPPING[@]}" ; do + SRC="${crd%%:*}" + DES="${crd##*:}" + cp "${VENDOR_DIR:-vendor}/github.com/openshift/api/machine/v1beta1/zz_generated.crd-manifests/$SRC" "config/crds/$DES" +done diff --git a/main.go b/main.go index f351c4d..b3df9b3 100644 --- a/main.go +++ b/main.go @@ -18,20 +18,32 @@ package main import ( "flag" + "fmt" + "net/http" "os" - - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - - _ "k8s.io/client-go/plugin/pkg/client/auth" - + "runtime/debug" + "time" + + "github.com/cloudscale-ch/cloudscale-go-sdk/v5" + configv1 "github.com/openshift/api/config/v1" + apifeatures "github.com/openshift/api/features" + machinev1beta1 "github.com/openshift/api/machine/v1beta1" + "github.com/openshift/library-go/pkg/features" + capimachine "github.com/openshift/machine-api-operator/pkg/controller/machine" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/util/feature" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/component-base/featuregate" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/server" - //+kubebuilder:scaffold:imports + + "github.com/appuio/machine-api-provider-cloudscale/controllers" + "github.com/appuio/machine-api-provider-cloudscale/pkg/machine" ) var ( @@ -41,6 +53,8 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(configv1.AddToScheme(scheme)) + utilruntime.Must(machinev1beta1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -56,17 +70,30 @@ func main() { flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + + var watchNamespace string + flag.StringVar(&watchNamespace, "namespace", "", "Namespace that the controller watches to reconcile machine-api objects. If unspecified, the controller watches for machine-api objects across all namespaces.") + opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) + + // TODO(bastjan): Check what those flags do. They are required since release-4.18 + featureGate := feature.DefaultMutableFeatureGate + gateOpts, err := features.NewFeatureGateOptions(featureGate, apifeatures.SelfManaged, apifeatures.FeatureGateMachineAPIMigration) + if err != nil { + setupLog.Error(err, "Error setting up feature gates") + } + gateOpts.AddFlagsToGoFlagSet(nil) + flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) switch target { case "manager": - runManager(metricsAddr, probeAddr, enableLeaderElection) + runManager(metricsAddr, probeAddr, watchNamespace, enableLeaderElection, featureGate) case "termination-handler": runTerminationHandler() default: @@ -75,8 +102,8 @@ func main() { } } -func runManager(metricsAddr, probeAddr string, enableLeaderElection bool) { - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ +func runManager(metricsAddr, probeAddr, watchNamespace string, enableLeaderElection bool, featureGate featuregate.MutableVersionedFeatureGate) { + opts := ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: metricsAddr, @@ -95,12 +122,66 @@ func runManager(metricsAddr, probeAddr string, enableLeaderElection bool) { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, - }) + + Cache: cache.Options{ + // Override the default 10 hour sync period so that we pick up external changes + // to the VMs within a reasonable time frame. + SyncPeriod: ptr.To(10 * time.Minute), + }, + } + + if watchNamespace != "" { + opts.Cache.DefaultNamespaces = map[string]cache.Config{ + watchNamespace: {}, + } + setupLog.Info("Watching machine-api objects only given namespace for reconciliation.", "namespace", watchNamespace) + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opts) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } + versionString := "unknown" + if v, ok := debug.ReadBuildInfo(); ok { + versionString = fmt.Sprintf("%s (%s)", v.Main.Version, v.GoVersion) + } + userAgent := "machine-api-provider-cloudscale.appuio.io/" + versionString + + newClient := func(token string) *cloudscale.Client { + cs := cloudscale.NewClient(http.DefaultClient) + cs.UserAgent = userAgent + cs.AuthToken = token + return cs + } + + machineActuator := machine.NewActuator(machine.ActuatorParams{ + K8sClient: mgr.GetClient(), + + DefaultCloudscaleAPIToken: os.Getenv("CLOUDSCALE_API_TOKEN"), + + ServerClientFactory: func(token string) cloudscale.ServerService { + return newClient(token).Servers + }, + ServerGroupClientFactory: func(token string) cloudscale.ServerGroupService { + return newClient(token).ServerGroups + }, + }) + + if err := capimachine.AddWithActuator(mgr, machineActuator, featureGate); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Machine") + os.Exit(1) + } + + if err := (&controllers.MachineSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MachineSet") + os.Exit(1) + } + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) diff --git a/pkg/machine/actuator.go b/pkg/machine/actuator.go new file mode 100644 index 0000000..50dc9f9 --- /dev/null +++ b/pkg/machine/actuator.go @@ -0,0 +1,489 @@ +package machine + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/cloudscale-ch/cloudscale-go-sdk/v5" + "github.com/google/go-jsonnet" + machinev1beta1 "github.com/openshift/api/machine/v1beta1" + machinecontroller "github.com/openshift/machine-api-operator/pkg/controller/machine" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + csv1beta1 "github.com/appuio/machine-api-provider-cloudscale/api/cloudscale/provider/v1beta1" +) + +const ( + antiAffinityTag = "machine-api-provider-cloudscale_appuio_io_antiAffinityKey" + machineNameTag = "machine-api-provider-cloudscale_appuio_io_name" +) + +// Actuator is responsible for performing machine reconciliation. +// It creates, updates, and deletes machines. +// Currently changing machine spec is not supported. +// The user data is rendered using Jsonnet with the machine and secret data as context. +// Machines are automatically spread across server groups on create based on the AntiAffinityKey. +type Actuator struct { + k8sClient client.Client + + defaultCloudscaleAPIToken string + + serverClientFactory func(token string) cloudscale.ServerService + serverGroupClientFactory func(token string) cloudscale.ServerGroupService +} + +// ActuatorParams holds parameter information for Actuator. +type ActuatorParams struct { + K8sClient client.Client + + DefaultCloudscaleAPIToken string + + ServerClientFactory func(token string) cloudscale.ServerService + ServerGroupClientFactory func(token string) cloudscale.ServerGroupService +} + +// NewActuator returns an actuator. +func NewActuator(params ActuatorParams) *Actuator { + return &Actuator{ + k8sClient: params.K8sClient, + + defaultCloudscaleAPIToken: params.DefaultCloudscaleAPIToken, + + serverClientFactory: params.ServerClientFactory, + serverGroupClientFactory: params.ServerGroupClientFactory, + } +} + +// Create creates a machine and is invoked by the machine controller. +func (a *Actuator) Create(ctx context.Context, machine *machinev1beta1.Machine) error { + l := log.FromContext(ctx).WithName("Actuator.Create") + + mctx, err := a.getMachineContext(ctx, machine) + if err != nil { + return fmt.Errorf("failed to get machine context: %w", err) + } + spec := mctx.spec + sc := a.serverClientFactory(mctx.token) + + userData, err := a.loadAndRenderUserDataSecret(ctx, mctx) + if err != nil { + return fmt.Errorf("failed to load user data secret: %w", err) + } + + // Null is not allowed for tags in the cloudscale API + if spec.Tags == nil { + spec.Tags = make(map[string]string) + } + spec.Tags[machineNameTag] = machine.Name + + // Null is not allowed for SSH keys in the cloudscale API + if spec.SSHKeys == nil { + spec.SSHKeys = []string{} + } + + serverGroups := spec.ServerGroups + if spec.AntiAffinityKey != "" { + sgc := a.serverGroupClientFactory(mctx.token) + aasg, err := a.ensureAntiAffinityServerGroupForKey(ctx, sgc, spec.Zone, spec.AntiAffinityKey) + if err != nil { + return fmt.Errorf("failed to ensure anti-affinity server group for machine %q and key %q: %w", machine.Name, spec.AntiAffinityKey, err) + } + serverGroups = append(serverGroups, aasg) + } + + name := machine.Name + if spec.BaseDomain != "" { + name = fmt.Sprintf("%s.%s", name, spec.BaseDomain) + } + + req := &cloudscale.ServerRequest{ + Name: name, + + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap(spec.Tags)), + }, + Zone: spec.Zone, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: spec.Zone, + }, + + Flavor: spec.Flavor, + Image: spec.Image, + VolumeSizeGB: spec.RootVolumeSizeGB, + Interfaces: cloudscaleServerInterfacesFromProviderSpecInterfaces(spec.Interfaces), + SSHKeys: spec.SSHKeys, + UseIPV6: spec.UseIPV6, + ServerGroups: serverGroups, + UserData: userData, + } + s, err := sc.Create(ctx, req) + if err != nil { + reqRaw, _ := json.Marshal(req) + return fmt.Errorf("failed to create machine %q: %w, req:%+v", machine.Name, err, string(reqRaw)) + } + + l.Info("Created machine", "machine", machine.Name, "uuid", s.UUID, "server", s) + + if err := updateMachineFromCloudscaleServer(machine, *s); err != nil { + return fmt.Errorf("failed to update machine %q from cloudscale API response: %w", machine.Name, err) + } + + if err := a.patchMachine(ctx, mctx.machine, machine); err != nil { + return fmt.Errorf("failed to patch machine %q: %w", machine.Name, err) + } + + return nil +} + +func (a *Actuator) Exists(ctx context.Context, machine *machinev1beta1.Machine) (bool, error) { + mctx, err := a.getMachineContext(ctx, machine) + if err != nil { + return false, fmt.Errorf("failed to get machine context: %w", err) + } + sc := a.serverClientFactory(mctx.token) + + s, err := a.getServer(ctx, sc, machine) + + return s != nil, err +} + +func (a *Actuator) Update(ctx context.Context, machine *machinev1beta1.Machine) error { + mctx, err := a.getMachineContext(ctx, machine) + if err != nil { + return fmt.Errorf("failed to get machine context: %w", err) + } + sc := a.serverClientFactory(mctx.token) + + s, err := a.getServer(ctx, sc, machine) + if err != nil { + return fmt.Errorf("failed to get server %q: %w", machine.Name, err) + } + + if err := updateMachineFromCloudscaleServer(machine, *s); err != nil { + return fmt.Errorf("failed to update machine %q from cloudscale API response: %w", machine.Name, err) + } + + if err := a.patchMachine(ctx, mctx.machine, machine); err != nil { + return fmt.Errorf("failed to patch machine %q: %w", machine.Name, err) + } + + return nil +} + +func (a *Actuator) Delete(ctx context.Context, machine *machinev1beta1.Machine) error { + l := log.FromContext(ctx).WithName("Actuator.Delete") + + mctx, err := a.getMachineContext(ctx, machine) + if err != nil { + return fmt.Errorf("failed to get machine context: %w", err) + } + sc := a.serverClientFactory(mctx.token) + + s, err := a.getServer(ctx, sc, machine) + if err != nil { + return fmt.Errorf("failed to get server %q: %w", machine.Name, err) + } + + if s == nil { + l.Info("Machine to delete not found, skipping", "machine", machine.Name) + return nil + } + + if err := sc.Delete(ctx, s.UUID); err != nil { + return fmt.Errorf("failed to delete server %q: %w", machine.Name, err) + } + + return nil +} + +func (a *Actuator) getServer(ctx context.Context, sc cloudscale.ServerService, machine *machinev1beta1.Machine) (*cloudscale.Server, error) { + lookupKey := cloudscale.TagMap{machineNameTag: machine.Name} + + ss, err := sc.List(ctx, cloudscale.WithTagFilter(lookupKey)) + if err != nil { + return nil, fmt.Errorf("failed to list servers: %w", err) + } + if len(ss) == 0 { + return nil, nil + } + if len(ss) > 1 { + return nil, fmt.Errorf("found multiple servers with name %q", machine.Name) + } + + return &ss[0], nil +} + +func (a *Actuator) patchMachine(ctx context.Context, orig, updated *machinev1beta1.Machine) error { + if equality.Semantic.DeepEqual(orig, updated) { + return nil + } + + st := *updated.Status.DeepCopy() + + if err := a.k8sClient.Patch(ctx, updated, client.MergeFrom(orig)); err != nil { + return fmt.Errorf("failed to patch machine %q: %w", updated.Name, err) + } + + updated.Status = st + if err := a.k8sClient.Status().Patch(ctx, updated, client.MergeFrom(orig)); err != nil { + return fmt.Errorf("failed to patch machine status %q: %w", updated.Name, err) + } + + return nil +} + +// ensureAntiAffinityServerGroupForKey ensures that a server group with less than 4 servers exists for the given key. +// If such a server group exists, its UUID is returned. +// If no such server group exists, a new server group is created and its UUID is returned. +func (a *Actuator) ensureAntiAffinityServerGroupForKey(ctx context.Context, sgc cloudscale.ServerGroupService, zone, key string) (string, error) { + l := log.FromContext(ctx).WithName("Actuator.ensureAntiAffinityServerGroupForKey").WithValues("key", key, "zone", zone) + lookupKey := cloudscale.TagMap{antiAffinityTag: key} + + sgs, err := sgc.List(ctx, cloudscale.WithTagFilter(lookupKey)) + if err != nil { + return "", fmt.Errorf("failed to list server groups: %w", err) + } + + for _, sg := range sgs { + if sg.Zone.Slug == zone { + if len(sg.Servers) < 4 { + l.Info("Found existing server group with less than 4 servers", "serverGroup", sg.UUID) + return sg.UUID, nil + } + } + } + + l.Info("No server group with less than 4 servers left, creating new server group") + sg, err := sgc.Create(ctx, &cloudscale.ServerGroupRequest{ + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(lookupKey), + }, + Name: key, + Type: "anti-affinity", + }) + if err != nil { + l.Error(err, "Failed to create server group") + return "", fmt.Errorf("failed to create server group: %w", err) + } + + return sg.UUID, nil +} + +func updateMachineFromCloudscaleServer(machine *machinev1beta1.Machine, s cloudscale.Server) error { + if machine.Labels == nil { + machine.Labels = make(map[string]string) + } + machine.Labels[machinecontroller.MachineInstanceTypeLabelName] = s.Flavor.Slug + machine.Labels[machinecontroller.MachineRegionLabelName] = strings.TrimRightFunc(s.Zone.Slug, func(r rune) bool { + return r >= '0' && r <= '9' + }) + machine.Labels[machinecontroller.MachineAZLabelName] = s.Zone.Slug + + machine.Spec.ProviderID = ptr.To(formatProviderID(s.UUID)) + machine.Status.Addresses = machineAddressesFromCloudscaleServer(s) + status := providerStatusFromCloudscaleServer(s) + rawStatus, err := csv1beta1.RawExtensionFromProviderStatus(&status) + if err != nil { + return fmt.Errorf("failed to create raw extension from provider status: %w", err) + } + machine.Status.ProviderStatus = rawStatus + + return nil +} + +func machineAddressesFromCloudscaleServer(s cloudscale.Server) []corev1.NodeAddress { + addresses := []corev1.NodeAddress{ + { + Type: corev1.NodeHostName, + Address: s.Name, + }, + { + Type: corev1.NodeInternalDNS, + Address: s.Name, + }, + } + + // https://github.com/openshift/cluster-machine-approver?tab=readme-ov-file#requirements-for-cluster-api-providers + // * A Machine must have a NodeInternalDNS set in Status.Addresses that matches the name of the Node. + // The NodeInternalDNS entry must be present, even before the Node resource is created. + // * A Machine must also have matching NodeInternalDNS, NodeExternalDNS, NodeHostName, NodeInternalIP, and NodeExternalIP addresses + // as those listed on the Node resource. All of these addresses are placed in the CSR and are validated against the addresses + // on the Machine object. + hostname := strings.Split(s.Name, ".")[0] + if s.Name != hostname { + addresses = append(addresses, corev1.NodeAddress{ + Type: corev1.NodeInternalDNS, + Address: hostname, + }) + } + + for _, n := range s.Interfaces { + typ := corev1.NodeInternalIP + if n.Type == "public" { + typ = corev1.NodeExternalIP + } + + for _, a := range n.Addresses { + addresses = append(addresses, corev1.NodeAddress{ + Type: typ, + Address: a.Address, + }) + } + } + + return addresses +} + +func formatProviderID(uuid string) string { + return fmt.Sprintf("cloudscale:///%s", uuid) +} + +func providerStatusFromCloudscaleServer(s cloudscale.Server) csv1beta1.CloudscaleMachineProviderStatus { + return csv1beta1.CloudscaleMachineProviderStatus{ + InstanceID: s.UUID, + Status: s.Status, + } +} + +func cloudscaleServerInterfacesFromProviderSpecInterfaces(interfaces []csv1beta1.Interface) *[]cloudscale.InterfaceRequest { + if interfaces == nil { + return nil + } + + var cloudscaleInterfaces []cloudscale.InterfaceRequest + for _, i := range interfaces { + if i.Type == csv1beta1.InterfaceTypePublic { + // From the cloudscale terraform provider. Public interfaces have no other configuration options. + // https://github.com/cloudscale-ch/terraform-provider-cloudscale/blob/56f5cb40396e489657ee965a5f066b8a9f5c1bd5/cloudscale/resource_cloudscale_server.go#L424-L427 + cloudscaleInterfaces = append(cloudscaleInterfaces, cloudscale.InterfaceRequest{ + Network: "public", + }) + continue + } + + ifr := cloudscale.InterfaceRequest{ + Network: i.NetworkUUID, + } + + if i.Addresses != nil { + addrs := make([]cloudscale.AddressRequest, 0, len(i.Addresses)) + for _, a := range i.Addresses { + addrs = append(addrs, cloudscale.AddressRequest{ + Subnet: a.SubnetUUID, + Address: a.Address, + }) + } + ifr.Addresses = &addrs + } + + cloudscaleInterfaces = append(cloudscaleInterfaces, ifr) + } + return &cloudscaleInterfaces +} + +type machineContext struct { + machine *machinev1beta1.Machine + spec csv1beta1.CloudscaleMachineProviderSpec + token string +} + +func (a *Actuator) getMachineContext(ctx context.Context, machine *machinev1beta1.Machine) (*machineContext, error) { + const tokenKey = "token" + + origMachine := machine.DeepCopy() + + spec, err := csv1beta1.ProviderSpecFromRawExtension(machine.Spec.ProviderSpec.Value) + if err != nil { + return nil, fmt.Errorf("failed to get provider spec from machine %q: %w", machine.Name, err) + } + + token := a.defaultCloudscaleAPIToken + if spec.TokenSecret != nil { + secret := &corev1.Secret{} + if err := a.k8sClient.Get(ctx, client.ObjectKey{Name: spec.TokenSecret.Name, Namespace: machine.Namespace}, secret); err != nil { + return nil, fmt.Errorf("failed to get secret %q: %w", spec.TokenSecret.Name, err) + } + + tb, ok := secret.Data[tokenKey] + if !ok { + return nil, fmt.Errorf("token key %q not found in secret %q", tokenKey, spec.TokenSecret.Name) + } + + token = string(tb) + } + + return &machineContext{ + machine: origMachine, + spec: *spec, + token: token, + }, nil +} + +func (a *Actuator) loadAndRenderUserDataSecret(ctx context.Context, mctx *machineContext) (string, error) { + const userDataKey = "userData" + + if mctx.spec.UserDataSecret == nil { + return "", nil + } + + secret := &corev1.Secret{} + if err := a.k8sClient.Get(ctx, client.ObjectKey{Name: mctx.spec.UserDataSecret.Name, Namespace: mctx.machine.Namespace}, secret); err != nil { + return "", fmt.Errorf("failed to get secret %q: %w", mctx.spec.UserDataSecret.Name, err) + } + + userDataRaw, ok := secret.Data[userDataKey] + if !ok { + return "", fmt.Errorf("%q key not found in secret %q", userDataKey, mctx.spec.UserDataSecret.Name) + } + userData := string(userDataRaw) + + if userData == "" { + return "", nil + } + + data := make(map[string]string, len(secret.Data)) + for k, v := range secret.Data { + data[k] = string(v) + } + + jvm, err := jsonnetVMWithContext(mctx.machine, data) + if err != nil { + return "", fmt.Errorf("userData: failed to create jsonnet VM: %w", err) + } + ud, err := jvm.EvaluateAnonymousSnippet("context", userData) + if err != nil { + return "", fmt.Errorf("userData: failed to evaluate jsonnet: %w", err) + } + + var compacted bytes.Buffer + if err := json.Compact(&compacted, []byte(ud)); err != nil { + return "", fmt.Errorf("userData: failed to compact json: %w", err) + } + + return compacted.String(), nil +} + +func jsonnetVMWithContext(machine *machinev1beta1.Machine, data map[string]string) (*jsonnet.VM, error) { + jcr, err := json.Marshal(map[string]any{ + "machine": machine, + "data": data, + }) + if err != nil { + return nil, fmt.Errorf("unable to marshal jsonnet context: %w", err) + } + jvm := jsonnet.MakeVM() + jvm.ExtCode("context", string(jcr)) + // Don't allow imports + jvm.Importer(&jsonnet.MemoryImporter{}) + return jvm, nil +} diff --git a/pkg/machine/actuator_test.go b/pkg/machine/actuator_test.go new file mode 100644 index 0000000..e0b5e5e --- /dev/null +++ b/pkg/machine/actuator_test.go @@ -0,0 +1,773 @@ +package machine + +import ( + "context" + "fmt" + "net/http/httptest" + "testing" + + "github.com/appuio/machine-api-provider-cloudscale/pkg/machine/csmock" + "github.com/cloudscale-ch/cloudscale-go-sdk/v5" + machinev1beta1 "github.com/openshift/api/machine/v1beta1" + machinecontroller "github.com/openshift/machine-api-operator/pkg/controller/machine" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + csv1beta1 "github.com/appuio/machine-api-provider-cloudscale/api/cloudscale/provider/v1beta1" +) + +func Test_Actuator_Create_ComplexMachineE2E(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + machine := &machinev1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + }, + } + providerSpec := csv1beta1.CloudscaleMachineProviderSpec{ + UserDataSecret: &corev1.LocalObjectReference{Name: "app-user-data"}, + TokenSecret: &corev1.LocalObjectReference{Name: "cloudscale-token"}, + BaseDomain: "cluster.example.com", + Zone: "rma1", + AntiAffinityKey: "app", + Flavor: "flex-16-4", + Image: "custom:rhcos-4.15", + RootVolumeSizeGB: 100, + Interfaces: []csv1beta1.Interface{ + { + Type: csv1beta1.InterfaceTypePrivate, + NetworkUUID: "6ad814b4-587f-44d2-96a1-38750c9a21d5", + Addresses: []csv1beta1.Address{ + { + SubnetUUID: "6ad814b4-587f-44d2-96a1-38750c9a21d5", + Address: "172.10.11.12", + }, + }, + }, { + Type: csv1beta1.InterfaceTypePublic, + }, + }, + } + setProviderSpecOnMachine(t, machine, &providerSpec) + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.TokenSecret.Name, + }, + Data: map[string][]byte{ + "token": []byte("my-cloudscale-token"), + }, + } + userDataSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.UserDataSecret.Name, + }, + Data: map[string][]byte{ + "ignitionCA": []byte("CADATA"), + "userData": []byte("{ca: std.extVar('context').data.ignitionCA}"), + }, + } + + c := newFakeClient(t, machine, tokenSecret, userDataSecret) + ss := csmock.NewMockServerService(ctrl) + sgs := csmock.NewMockServerGroupService(ctrl) + actuator := newActuator(c, ss, sgs) + + sgs.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{antiAffinityTag: providerSpec.AntiAffinityKey}}, + ).Return([]cloudscale.ServerGroup{}, nil) + + sgs.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerGroupRequest{ + Name: providerSpec.AntiAffinityKey, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: &cloudscale.TagMap{ + antiAffinityTag: providerSpec.AntiAffinityKey, + }, + }, + Type: "anti-affinity", + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: providerSpec.Zone, + }, + }), + ).Return(&cloudscale.ServerGroup{ + UUID: "created-server-group-uuid", + }, nil) + + ss.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerRequest{ + Name: fmt.Sprintf("%s.%s", machine.Name, providerSpec.BaseDomain), + + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap{ + machineNameTag: machine.Name, + }), + }, + Zone: providerSpec.Zone, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: providerSpec.Zone, + }, + + Flavor: providerSpec.Flavor, + Image: providerSpec.Image, + VolumeSizeGB: providerSpec.RootVolumeSizeGB, + Interfaces: ptr.To([]cloudscale.InterfaceRequest{ + { + Network: providerSpec.Interfaces[0].NetworkUUID, + Addresses: &[]cloudscale.AddressRequest{{ + Subnet: providerSpec.Interfaces[0].Addresses[0].SubnetUUID, + Address: providerSpec.Interfaces[0].Addresses[0].Address, + }}, + }, { + Network: "public", + }, + }), + SSHKeys: []string{}, + UseIPV6: providerSpec.UseIPV6, + ServerGroups: []string{"created-server-group-uuid"}, + UserData: "{\"ca\":\"CADATA\"}", + }), + ).DoAndReturn(cloudscaleServerFromServerRequest(func(s *cloudscale.Server) { + s.UUID = "created-server-uuid" + })) + + require.NoError(t, actuator.Create(ctx, machine)) + + updatedMachine := &machinev1beta1.Machine{} + require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(machine), updatedMachine)) + if assert.NotNil(t, updatedMachine.Spec.ProviderID) { + assert.Equal(t, "cloudscale:///created-server-uuid", *updatedMachine.Spec.ProviderID) + } + + // Labels are just for show with kubectl get + if assert.NotNil(t, updatedMachine.Labels) { + assert.Equal(t, "flex-16-4", updatedMachine.Labels[machinecontroller.MachineInstanceTypeLabelName]) + assert.Equal(t, "rma1", updatedMachine.Labels[machinecontroller.MachineAZLabelName]) + assert.Equal(t, "rma", updatedMachine.Labels[machinecontroller.MachineRegionLabelName]) + } + + // https://github.com/openshift/cluster-machine-approver?tab=readme-ov-file#requirements-for-cluster-api-providers + // * A Machine must have a NodeInternalDNS set in Status.Addresses that matches the name of the Node. + // The NodeInternalDNS entry must be present, even before the Node resource is created. + // * A Machine must also have matching NodeInternalDNS, NodeExternalDNS, NodeHostName, NodeInternalIP, and NodeExternalIP addresses + // as those listed on the Node resource. All of these addresses are placed in the CSR and are validated against the addresses + // on the Machine object. + assert.ElementsMatch(t, []corev1.NodeAddress{ + { + Type: corev1.NodeHostName, + Address: "app-test.cluster.example.com", + }, + { + Type: corev1.NodeInternalDNS, + Address: "app-test", + }, + { + Type: corev1.NodeInternalDNS, + Address: "app-test.cluster.example.com", + }, + { + Type: corev1.NodeInternalIP, + Address: "172.10.11.12", + }, + }, updatedMachine.Status.Addresses) +} + +func Test_Actuator_Create_AntiAffinityPools(t *testing.T) { + const zone = "rma1" + + tcs := []struct { + name string + apiMock func(*testing.T, *machinev1beta1.Machine, csv1beta1.CloudscaleMachineProviderSpec, *csmock.MockServerService, *csmock.MockServerGroupService) + err error + }{ + { + name: "no anti-affinity pool exists", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ps csv1beta1.CloudscaleMachineProviderSpec, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + const newSGUUID = "new-server-group-uuid" + sgs.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{antiAffinityTag: ps.AntiAffinityKey}}, + ).Return([]cloudscale.ServerGroup{}, nil) + sgs.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerGroupRequest{ + Name: ps.AntiAffinityKey, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: &cloudscale.TagMap{ + antiAffinityTag: ps.AntiAffinityKey, + }, + }, + Type: "anti-affinity", + }), + ).Return(&cloudscale.ServerGroup{ + UUID: newSGUUID, + }, nil) + ss.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerRequest{ + Name: machine.Name, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap{ + machineNameTag: machine.Name, + }), + }, + ServerGroups: []string{newSGUUID}, + SSHKeys: []string{}, + Zone: zone, + }), + ).Return(&cloudscale.Server{}, nil) + }, + }, + { + name: "pool with space left exists", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ps csv1beta1.CloudscaleMachineProviderSpec, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + const existingUUID = "existing-server-group-uuid" + sgs.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{antiAffinityTag: ps.AntiAffinityKey}}, + ).Return([]cloudscale.ServerGroup{ + { + UUID: existingUUID, + Servers: make([]cloudscale.ServerStub, 3), + ZonalResource: cloudscale.ZonalResource{ + Zone: cloudscale.Zone{ + Slug: zone, + }, + }, + }, + }, nil) + ss.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerRequest{ + Name: machine.Name, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap{ + machineNameTag: machine.Name, + }), + }, + ServerGroups: []string{existingUUID}, + SSHKeys: []string{}, + Zone: zone, + }), + ).Return(&cloudscale.Server{}, nil) + }, + }, + { + name: "pool with no space left exists, create new pool", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ps csv1beta1.CloudscaleMachineProviderSpec, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + const newSGUUID = "new-server-group-uuid" + sgs.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{antiAffinityTag: ps.AntiAffinityKey}}, + ).Return([]cloudscale.ServerGroup{ + { + UUID: "existing-server-group-uuid", + Servers: make([]cloudscale.ServerStub, 4), + ZonalResource: cloudscale.ZonalResource{ + Zone: cloudscale.Zone{ + Slug: zone, + }, + }, + }, + }, nil) + sgs.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerGroupRequest{ + Name: ps.AntiAffinityKey, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: &cloudscale.TagMap{ + antiAffinityTag: ps.AntiAffinityKey, + }, + }, + Type: "anti-affinity", + }), + ).Return(&cloudscale.ServerGroup{ + UUID: newSGUUID, + }, nil) + ss.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerRequest{ + Name: machine.Name, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap{ + machineNameTag: machine.Name, + }), + }, + ServerGroups: []string{newSGUUID}, + SSHKeys: []string{}, + Zone: zone, + }), + ).Return(&cloudscale.Server{}, nil) + }, + }, + { + name: "pool with space exists in wrong zone, create new pool", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ps csv1beta1.CloudscaleMachineProviderSpec, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + const newSGUUID = "new-server-group-uuid" + sgs.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{antiAffinityTag: ps.AntiAffinityKey}}, + ).Return([]cloudscale.ServerGroup{ + { + UUID: "existing-server-group-uuid", + Servers: make([]cloudscale.ServerStub, 0), + ZonalResource: cloudscale.ZonalResource{ + Zone: cloudscale.Zone{ + Slug: "other-zone", + }, + }, + }, + }, nil) + sgs.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerGroupRequest{ + Name: ps.AntiAffinityKey, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: &cloudscale.TagMap{ + antiAffinityTag: ps.AntiAffinityKey, + }, + }, + Type: "anti-affinity", + }), + ).Return(&cloudscale.ServerGroup{ + UUID: newSGUUID, + }, nil) + ss.EXPECT().Create( + gomock.Any(), + newDeepEqualMatcher(t, &cloudscale.ServerRequest{ + Name: machine.Name, + ZonalResourceRequest: cloudscale.ZonalResourceRequest{ + Zone: zone, + }, + TaggedResourceRequest: cloudscale.TaggedResourceRequest{ + Tags: ptr.To(cloudscale.TagMap{ + machineNameTag: machine.Name, + }), + }, + ServerGroups: []string{newSGUUID}, + SSHKeys: []string{}, + Zone: zone, + }), + ).Return(&cloudscale.Server{}, nil) + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + machine := &machinev1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + }, + } + providerSpec := csv1beta1.CloudscaleMachineProviderSpec{ + TokenSecret: &corev1.LocalObjectReference{Name: "cloudscale-token"}, + Zone: zone, + AntiAffinityKey: "app", + } + setProviderSpecOnMachine(t, machine, &providerSpec) + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.TokenSecret.Name, + }, + Data: map[string][]byte{ + "token": []byte("my-cloudscale-token"), + }, + } + + c := newFakeClient(t, machine, tokenSecret) + ss := csmock.NewMockServerService(ctrl) + sgs := csmock.NewMockServerGroupService(ctrl) + actuator := newActuator(c, ss, sgs) + + tc.apiMock(t, machine, providerSpec, ss, sgs) + + require.NoError(t, actuator.Create(ctx, machine)) + }) + } +} + +func Test_Actuator_Exists(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + servers []cloudscale.Server + exists bool + }{ + { + name: "machine exists", + servers: []cloudscale.Server{ + { + Name: "app-test", + }, + }, + exists: true, + }, + { + name: "machine does not exist", + servers: []cloudscale.Server{}, + exists: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + machine := &machinev1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + }, + } + providerSpec := csv1beta1.CloudscaleMachineProviderSpec{ + TokenSecret: &corev1.LocalObjectReference{Name: "cloudscale-token"}, + } + setProviderSpecOnMachine(t, machine, &providerSpec) + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.TokenSecret.Name, + }, + Data: map[string][]byte{ + "token": []byte("my-cloudscale-token"), + }, + } + + c := newFakeClient(t, machine, tokenSecret) + ss := csmock.NewMockServerService(ctrl) + sgs := csmock.NewMockServerGroupService(ctrl) + actuator := newActuator(c, ss, sgs) + + ss.EXPECT().List(ctx, csTagMatcher{t: t, tags: map[string]string{machineNameTag: machine.Name}}).Return(tc.servers, nil) + + exists, err := actuator.Exists(ctx, machine) + require.NoError(t, err) + assert.Equal(t, tc.exists, exists) + }) + } +} + +func Test_Actuator_Update(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + machine := &machinev1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + }, + } + providerSpec := csv1beta1.CloudscaleMachineProviderSpec{ + TokenSecret: &corev1.LocalObjectReference{Name: "cloudscale-token"}, + } + setProviderSpecOnMachine(t, machine, &providerSpec) + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.TokenSecret.Name, + }, + Data: map[string][]byte{ + "token": []byte("my-cloudscale-token"), + }, + } + + c := newFakeClient(t, machine, tokenSecret) + ss := csmock.NewMockServerService(ctrl) + sgs := csmock.NewMockServerGroupService(ctrl) + actuator := newActuator(c, ss, sgs) + + ss.EXPECT().List(ctx, csTagMatcher{ + t: t, + tags: map[string]string{machineNameTag: machine.Name}, + }).Return([]cloudscale.Server{{ + UUID: "machine-uuid", + }}, nil) + + require.NoError(t, actuator.Update(ctx, machine)) + + var updatedMachine machinev1beta1.Machine + require.NoError(t, c.Get(ctx, client.ObjectKeyFromObject(machine), &updatedMachine)) + if assert.NotNil(t, updatedMachine.Spec.ProviderID) { + assert.Equal(t, "cloudscale:///machine-uuid", *updatedMachine.Spec.ProviderID) + } +} + +func Test_Actuator_Delete(t *testing.T) { + t.Parallel() + + tcs := []struct { + name string + apiMock func(*testing.T, *machinev1beta1.Machine, *csmock.MockServerService, *csmock.MockServerGroupService) + err error + }{ + { + name: "machine exists", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + ss.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{machineNameTag: machine.Name}}, + ).Return([]cloudscale.Server{ + { + UUID: "machine-uuid", + }, + }, nil) + ss.EXPECT().Delete( + gomock.Any(), + "machine-uuid", + ).Return(nil) + }, + }, { + name: "machine does not exist", + apiMock: func(t *testing.T, machine *machinev1beta1.Machine, ss *csmock.MockServerService, sgs *csmock.MockServerGroupService) { + ss.EXPECT().List( + gomock.Any(), + csTagMatcher{t: t, tags: map[string]string{machineNameTag: machine.Name}}, + ).Return([]cloudscale.Server{}, nil) + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + machine := &machinev1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-test", + }, + } + providerSpec := csv1beta1.CloudscaleMachineProviderSpec{ + TokenSecret: &corev1.LocalObjectReference{Name: "cloudscale-token"}, + } + setProviderSpecOnMachine(t, machine, &providerSpec) + tokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: providerSpec.TokenSecret.Name, + }, + Data: map[string][]byte{ + "token": []byte("my-cloudscale-token"), + }, + } + + c := newFakeClient(t, machine, tokenSecret) + ss := csmock.NewMockServerService(ctrl) + sgs := csmock.NewMockServerGroupService(ctrl) + actuator := newActuator(c, ss, sgs) + + tc.apiMock(t, machine, ss, sgs) + + require.Equal(t, tc.err, actuator.Delete(ctx, machine)) + }) + } +} + +// cloudscaleServerFromServerRequest returns a function that creates a cloudscale.Server from a cloudscale.ServerRequest +// The returned server can be modified by the callback function before being returned. +func cloudscaleServerFromServerRequest(cb func(*cloudscale.Server)) func(_ context.Context, req *cloudscale.ServerRequest) (*cloudscale.Server, error) { + return func(_ context.Context, req *cloudscale.ServerRequest) (*cloudscale.Server, error) { + reqIntfs := req.Interfaces + if reqIntfs == nil { + reqIntfs = &[]cloudscale.InterfaceRequest{} + } + intfs := []cloudscale.Interface{} + for _, i := range *reqIntfs { + sif := cloudscale.Interface{ + Network: cloudscale.NetworkStub{ + UUID: i.Network, + }, + } + if i.Addresses != nil { + for _, a := range *i.Addresses { + sif.Addresses = append(sif.Addresses, cloudscale.Address{ + Address: a.Address, + Subnet: cloudscale.SubnetStub{ + UUID: a.Subnet, + }, + }) + } + } + intfs = append(intfs, sif) + } + + s := &cloudscale.Server{ + ZonalResource: cloudscale.ZonalResource{ + Zone: cloudscale.Zone{ + Slug: req.ZonalResourceRequest.Zone, + }, + }, + TaggedResource: cloudscale.TaggedResource{ + Tags: ptr.Deref(req.TaggedResourceRequest.Tags, cloudscale.TagMap{}), + }, + HREF: "https://some-ref", + UUID: "UUID", + Name: req.Name, + Status: "running", + Flavor: cloudscale.Flavor{ + Slug: req.Flavor, + }, + Image: cloudscale.Image{ + Slug: req.Image, + }, + Interfaces: intfs, + } + cb(s) + return s, nil + } +} + +// assertLogStub is a stub that implements the assert.TestingT interface but doe not fail the test. It only logs the error message to the test log. +type assertLogStub struct { + t *testing.T +} + +func (s assertLogStub) Errorf(format string, args ...interface{}) { + s.t.Logf(format, args...) +} + +func (c assertLogStub) FailNow() { + panic("assertLogStub.FailNow called") +} + +// DeepEqualMatcher uses testify/assert.Equal to compare the expected and actual values and print a meaningful error message if something fails +// It does not fail the test itself, but logs the error message to the test log. +type deepEqualMatcher struct { + t assert.TestingT + comp any +} + +// newDeepEqualMatcher creates a new deepEqualMatcher +func newDeepEqualMatcher(t *testing.T, comp any) *deepEqualMatcher { + return &deepEqualMatcher{t: assertLogStub{t}, comp: comp} +} + +func (m deepEqualMatcher) Matches(x any) bool { + return assert.Equal(m.t, m.comp, x) +} + +func (m deepEqualMatcher) String() string { + return fmt.Sprint("is equal to", m.comp) +} + +type csTagMatcher struct { + t *testing.T + tags map[string]string +} + +func (m csTagMatcher) Matches(x any) bool { + tmf, ok := x.(cloudscale.ListRequestModifier) + if !ok { + m.t.Logf("expected cloudscale.ListRequestModifier, got %T", x) + return false + } + + req := httptest.NewRequest("GET", "http://example.com", nil) + tmf(req) + + matches := true + for key, value := range m.tags { + matches = matches && assert.Contains(assertLogStub{m.t}, req.URL.RawQuery, fmt.Sprintf("tag%%3A%s=%s", key, value)) + } + + return matches +} + +func (m csTagMatcher) String() string { + return fmt.Sprint("matches function(*http.Request) adding to query:", m.tags) +} + +func setProviderSpecOnMachine(t *testing.T, machine *machinev1beta1.Machine, providerSpec *csv1beta1.CloudscaleMachineProviderSpec) { + t.Helper() + + ext, err := csv1beta1.RawExtensionFromProviderSpec(providerSpec) + require.NoError(t, err) + machine.Spec.ProviderSpec.Value = ext +} + +func newActuator(c client.Client, ss cloudscale.ServerService, sgs cloudscale.ServerGroupService) *Actuator { + return NewActuator(ActuatorParams{ + K8sClient: c, + DefaultCloudscaleAPIToken: "", + ServerClientFactory: func(token string) cloudscale.ServerService { + return ss + }, + ServerGroupClientFactory: func(token string) cloudscale.ServerGroupService { + return sgs + }, + }) +} + +var testScheme = func() *runtime.Scheme { + must := func(err error) { + if err != nil { + panic(err) + } + } + scheme := runtime.NewScheme() + must(clientgoscheme.AddToScheme(scheme)) + must(machinev1beta1.AddToScheme(scheme)) + return scheme +}() + +func newFakeClient(t *testing.T, initObjs ...runtime.Object) client.Client { + t.Helper() + + return fake.NewClientBuilder(). + WithScheme(testScheme). + WithRuntimeObjects(initObjs...). + WithStatusSubresource( + &machinev1beta1.Machine{}, + ). + Build() +} diff --git a/pkg/machine/csmock/server_group_service.go b/pkg/machine/csmock/server_group_service.go new file mode 100644 index 0000000..84c6dbc --- /dev/null +++ b/pkg/machine/csmock/server_group_service.go @@ -0,0 +1,120 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/cloudscale-ch/cloudscale-go-sdk/v5 (interfaces: ServerGroupService) +// +// Generated by this command: +// +// mockgen -destination=./csmock/server_group_service.go -package csmock github.com/cloudscale-ch/cloudscale-go-sdk/v5 ServerGroupService +// + +// Package csmock is a generated GoMock package. +package csmock + +import ( + context "context" + reflect "reflect" + + cloudscale "github.com/cloudscale-ch/cloudscale-go-sdk/v5" + gomock "go.uber.org/mock/gomock" +) + +// MockServerGroupService is a mock of ServerGroupService interface. +type MockServerGroupService struct { + ctrl *gomock.Controller + recorder *MockServerGroupServiceMockRecorder + isgomock struct{} +} + +// MockServerGroupServiceMockRecorder is the mock recorder for MockServerGroupService. +type MockServerGroupServiceMockRecorder struct { + mock *MockServerGroupService +} + +// NewMockServerGroupService creates a new mock instance. +func NewMockServerGroupService(ctrl *gomock.Controller) *MockServerGroupService { + mock := &MockServerGroupService{ctrl: ctrl} + mock.recorder = &MockServerGroupServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockServerGroupService) EXPECT() *MockServerGroupServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockServerGroupService) Create(ctx context.Context, createRequest *cloudscale.ServerGroupRequest) (*cloudscale.ServerGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, createRequest) + ret0, _ := ret[0].(*cloudscale.ServerGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockServerGroupServiceMockRecorder) Create(ctx, createRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockServerGroupService)(nil).Create), ctx, createRequest) +} + +// Delete mocks base method. +func (m *MockServerGroupService) Delete(ctx context.Context, serverGroupID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, serverGroupID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockServerGroupServiceMockRecorder) Delete(ctx, serverGroupID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockServerGroupService)(nil).Delete), ctx, serverGroupID) +} + +// Get mocks base method. +func (m *MockServerGroupService) Get(ctx context.Context, serverGroupID string) (*cloudscale.ServerGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, serverGroupID) + ret0, _ := ret[0].(*cloudscale.ServerGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockServerGroupServiceMockRecorder) Get(ctx, serverGroupID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockServerGroupService)(nil).Get), ctx, serverGroupID) +} + +// List mocks base method. +func (m *MockServerGroupService) List(ctx context.Context, modifiers ...cloudscale.ListRequestModifier) ([]cloudscale.ServerGroup, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range modifiers { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].([]cloudscale.ServerGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockServerGroupServiceMockRecorder) List(ctx any, modifiers ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, modifiers...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockServerGroupService)(nil).List), varargs...) +} + +// Update mocks base method. +func (m *MockServerGroupService) Update(ctx context.Context, networkID string, updateRequest *cloudscale.ServerGroupRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, networkID, updateRequest) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockServerGroupServiceMockRecorder) Update(ctx, networkID, updateRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockServerGroupService)(nil).Update), ctx, networkID, updateRequest) +} diff --git a/pkg/machine/csmock/server_service.go b/pkg/machine/csmock/server_service.go new file mode 100644 index 0000000..0a25439 --- /dev/null +++ b/pkg/machine/csmock/server_service.go @@ -0,0 +1,162 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/cloudscale-ch/cloudscale-go-sdk/v5 (interfaces: ServerService) +// +// Generated by this command: +// +// mockgen -destination=./csmock/server_service.go -package csmock github.com/cloudscale-ch/cloudscale-go-sdk/v5 ServerService +// + +// Package csmock is a generated GoMock package. +package csmock + +import ( + context "context" + reflect "reflect" + + cloudscale "github.com/cloudscale-ch/cloudscale-go-sdk/v5" + gomock "go.uber.org/mock/gomock" +) + +// MockServerService is a mock of ServerService interface. +type MockServerService struct { + ctrl *gomock.Controller + recorder *MockServerServiceMockRecorder + isgomock struct{} +} + +// MockServerServiceMockRecorder is the mock recorder for MockServerService. +type MockServerServiceMockRecorder struct { + mock *MockServerService +} + +// NewMockServerService creates a new mock instance. +func NewMockServerService(ctrl *gomock.Controller) *MockServerService { + mock := &MockServerService{ctrl: ctrl} + mock.recorder = &MockServerServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockServerService) EXPECT() *MockServerServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockServerService) Create(ctx context.Context, createRequest *cloudscale.ServerRequest) (*cloudscale.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, createRequest) + ret0, _ := ret[0].(*cloudscale.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockServerServiceMockRecorder) Create(ctx, createRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockServerService)(nil).Create), ctx, createRequest) +} + +// Delete mocks base method. +func (m *MockServerService) Delete(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockServerServiceMockRecorder) Delete(ctx, serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockServerService)(nil).Delete), ctx, serverID) +} + +// Get mocks base method. +func (m *MockServerService) Get(ctx context.Context, serverID string) (*cloudscale.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, serverID) + ret0, _ := ret[0].(*cloudscale.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockServerServiceMockRecorder) Get(ctx, serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockServerService)(nil).Get), ctx, serverID) +} + +// List mocks base method. +func (m *MockServerService) List(ctx context.Context, modifiers ...cloudscale.ListRequestModifier) ([]cloudscale.Server, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range modifiers { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].([]cloudscale.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockServerServiceMockRecorder) List(ctx any, modifiers ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, modifiers...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockServerService)(nil).List), varargs...) +} + +// Reboot mocks base method. +func (m *MockServerService) Reboot(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reboot", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Reboot indicates an expected call of Reboot. +func (mr *MockServerServiceMockRecorder) Reboot(ctx, serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reboot", reflect.TypeOf((*MockServerService)(nil).Reboot), ctx, serverID) +} + +// Start mocks base method. +func (m *MockServerService) Start(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start. +func (mr *MockServerServiceMockRecorder) Start(ctx, serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockServerService)(nil).Start), ctx, serverID) +} + +// Stop mocks base method. +func (m *MockServerService) Stop(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop. +func (mr *MockServerServiceMockRecorder) Stop(ctx, serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockServerService)(nil).Stop), ctx, serverID) +} + +// Update mocks base method. +func (m *MockServerService) Update(ctx context.Context, serverID string, updateRequest *cloudscale.ServerUpdateRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, serverID, updateRequest) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockServerServiceMockRecorder) Update(ctx, serverID, updateRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockServerService)(nil).Update), ctx, serverID, updateRequest) +} diff --git a/pkg/machine/mock.go b/pkg/machine/mock.go new file mode 100644 index 0000000..9f0564e --- /dev/null +++ b/pkg/machine/mock.go @@ -0,0 +1,4 @@ +package machine + +//go:generate go run go.uber.org/mock/mockgen -destination=./csmock/server_service.go -package csmock github.com/cloudscale-ch/cloudscale-go-sdk/v5 ServerService +//go:generate go run go.uber.org/mock/mockgen -destination=./csmock/server_group_service.go -package csmock github.com/cloudscale-ch/cloudscale-go-sdk/v5 ServerGroupService diff --git a/pkg/machine/userdata/secret.jsonnet b/pkg/machine/userdata/secret.jsonnet new file mode 100644 index 0000000..4a9f252 --- /dev/null +++ b/pkg/machine/userdata/secret.jsonnet @@ -0,0 +1,14 @@ +{ + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'cloudscale-user-data', + namespace: 'openshift-machine-api', + }, + stringData: { + ignitionHost: std.extVar('ignitionHost'), + ignitionCA: std.extVar('ignitionCA'), + userData: (importstr './userdata.jsonnet'), + }, + type: 'Opaque', +} diff --git a/pkg/machine/userdata/userdata-secret-from-maintfjson.sh b/pkg/machine/userdata/userdata-secret-from-maintfjson.sh new file mode 100755 index 0000000..691d501 --- /dev/null +++ b/pkg/machine/userdata/userdata-secret-from-maintfjson.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# This script is used to generate the userdata file from the main.tf.json output by component openshift4-terraform +# First optional positional argument is the path to the main.tf.json file + +set -euo pipefail + +maintf="${1:-main.tf.json}" +sdir="$(dirname "$(readlink -f "$0")")" + +ignitionCA="$(jq -er '.module.cluster.ignition_ca' "$maintf")" +ignitionHost="api-int.$(jq -er '.module.cluster.cluster_name' "$maintf").$(jq -er '.module.cluster.base_domain' "$maintf")" + +jsonnet "${sdir}/secret.jsonnet" --ext-str "ignitionHost=${ignitionHost}" --ext-str "ignitionCA=${ignitionCA}" diff --git a/pkg/machine/userdata/userdata.jsonnet b/pkg/machine/userdata/userdata.jsonnet new file mode 100644 index 0000000..4774e9c --- /dev/null +++ b/pkg/machine/userdata/userdata.jsonnet @@ -0,0 +1,36 @@ +local context = std.extVar('context'); + +{ + ignition: { + version: '3.1.0', + config: { + merge: [ { + source: 'https://%s:22623/config/%s' % [ context.data.ignitionHost, std.get(context.data, 'ignitionConfigName', 'worker') ], + } ], + }, + security: { + tls: { + certificateAuthorities: [ { + source: 'data:text/plain;charset=utf-8;base64,%s' % [ std.base64(context.data.ignitionCA) ], + } ], + }, + }, + }, + systemd: { + units: [ { + name: 'cloudscale-hostkeys.service', + enabled: true, + contents: "[Unit]\nDescription=Print SSH Public Keys to tty\nAfter=sshd-keygen.target\n\n[Install]\nWantedBy=multi-user.target\n\n[Service]\nType=oneshot\nStandardOutput=tty\nTTYPath=/dev/ttyS0\nExecStart=/bin/sh -c \"echo '-----BEGIN SSH HOST KEY KEYS-----'; cat /etc/ssh/ssh_host_*key.pub; echo '-----END SSH HOST KEY KEYS-----'\"", + } ], + }, + storage: { + files: [ { + filesystem: 'root', + path: '/etc/hostname', + mode: 420, + contents: { + source: 'data:,%s' % context.machine.metadata.name, + }, + } ], + }, +} diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..0d943de --- /dev/null +++ b/tools.go @@ -0,0 +1,17 @@ +//go:build tools +// +build tools + +// Package tools is a place to put any tooling dependencies as imports. +// Go modules will be forced to download and install them. +package tools + +import ( + // We sync the required manifests to run the controller from here + _ "github.com/openshift/api/machine/v1beta1/zz_generated.crd-manifests" + + // This is basically KubeBuilder + _ "sigs.k8s.io/controller-tools/cmd/controller-gen" + + // Used to mock the cloudscale Go client + _ "go.uber.org/mock/mockgen" +)