diff --git a/.go-version b/.go-version index aafdde18..0e0c284d 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.23.11 +1.25.9 diff --git a/.golangci.yml b/.golangci.yml index 5eb9adf4..95c18aa3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -89,16 +89,22 @@ linters-settings: - pkg: sigs.k8s.io/controller-runtime/pkg/controller/controllerutil alias: ctrlutil # CAPI - - pkg: sigs.k8s.io/cluster-api/api/v1beta1 + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta1 alias: clusterv1 + - pkg: sigs.k8s.io/cluster-api/api/core/v1beta2 + alias: clusterv2 - pkg: sigs.k8s.io/cluster-api/errors alias: capierrors - pkg: sigs.k8s.io/cluster-api/util alias: capiutil - - pkg: sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1 + - pkg: sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta1 alias: bootstrapv1 - - pkg: sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1 + - pkg: sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta2 + alias: bootstrapv2 + - pkg: sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1 alias: controlplanev1 + - pkg: sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2 + alias: controlplanev2 - pkg: sigs.k8s.io/cluster-api/test/e2e alias: capie2e # CAPE @@ -167,18 +173,32 @@ issues: text: should not use dot imports - path: _test\.go text: cyclomatic complexity - # Deprecations for FailureReason + # CAPI v1beta1 deprecations are currently tolerated globally while the project is still on v1beta1 APIs. - linters: - staticcheck - text: "SA1019: \"sigs.k8s.io/cluster-api/errors\" is deprecated:" - # Deprecations for FailureMessage, FailureReason, UnavailableReplicas + text: 'SA1019: "sigs.k8s.io/cluster-api/errors" is deprecated:' - linters: - - staticcheck - text: "SA1019: .*\\.Status\\.(FailureMessage|FailureReason|UnavailableReplicas) is deprecated: This field is deprecated and is going to be removed in the next apiVersion. Please see https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20240916-improve-status-in-CAPI-resources.md for more details." + - staticcheck + text: 'SA1019: "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta1" is deprecated:' + - linters: + - staticcheck + text: 'SA1019: "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" is deprecated:' + - linters: + - staticcheck + text: 'SA1019: "sigs.k8s.io/cluster-api/api/core/v1beta1" is deprecated:' + - linters: + - staticcheck + text: 'SA1019: "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" is deprecated:' + - linters: + - staticcheck + text: 'SA1019: "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" is deprecated:' + - linters: + - staticcheck + text: 'SA1019: .*\.Status\.(FailureMessage|FailureReason|UnavailableReplicas) is deprecated:' run: timeout: 10m - go: "1.23" + go: "1.25" skip-files: - "zz_generated.*\\.go$" allow-parallel-runners: true diff --git a/Dockerfile b/Dockerfile index cbb6c59d..fcefa937 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ARG ARCH # Build the manager binary -FROM golang:1.23.11 as builder +FROM golang:1.25.9 as builder WORKDIR /workspace # Run this with docker build --build_arg $(go env GOPROXY) to override the goproxy diff --git a/Makefile b/Makefile index 0f5728f1..8ce3ffc2 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ VERSION ?= $(shell cat clusterctl-settings.json | jq .config.nextVersion -r) # # Go. # -GO_VERSION ?= 1.23.11 +GO_VERSION ?= 1.25.9 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -163,7 +163,7 @@ kustomize: ## Download kustomize locally if necessary. CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. - $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.17.2) + $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.18.0) GINKGO := $(shell pwd)/bin/ginkgo ginkgo: ## Download ginkgo locally if necessary. diff --git a/api/v1beta1/conditions_consts.go b/api/v1beta1/conditions_consts.go index b9b4e163..1b81f352 100644 --- a/api/v1beta1/conditions_consts.go +++ b/api/v1beta1/conditions_consts.go @@ -16,7 +16,9 @@ limitations under the License. package v1beta1 -import clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +import ( + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) // Conditions and condition Reasons for the ElfMachine object. diff --git a/api/v1beta1/consts.go b/api/v1beta1/consts.go index 5f1eccb6..2c78b7b8 100644 --- a/api/v1beta1/consts.go +++ b/api/v1beta1/consts.go @@ -60,6 +60,22 @@ const ( // It is the name of compute cluster where the virtual machine runs on. ComputeClusterNameLabel = "cape.infrastructure.cluster.x-k8s.io/compute-cluster-name" + // StorageClusterIDLabel is the label set on nodes. + // It is the ID of the storage cluster where the virtual machine's storage resides. + StorageClusterIDLabel = "cape.infrastructure.cluster.x-k8s.io/storage-cluster-id" + + // StorageClusterNameLabel is the label set on nodes. + // It is the name of the storage cluster where the virtual machine's storage resides. + StorageClusterNameLabel = "cape.infrastructure.cluster.x-k8s.io/storage-cluster-name" + + // DataStoreIDLabel is the label set on nodes. + // It is the ID of the datastore where the virtual machine's storage resides. + DataStoreIDLabel = "cape.infrastructure.cluster.x-k8s.io/datastore-id" + + // DataStoreNameLabel is the label set on nodes. + // It is the name of the datastore where the virtual machine's storage resides. + DataStoreNameLabel = "cape.infrastructure.cluster.x-k8s.io/datastore-name" + // ZoneIDLabel is the label set on nodes. // It is the Tower ID of zone where the virtual machine runs on. ZoneIDLabel = "cape.infrastructure.cluster.x-k8s.io/zone-id" diff --git a/api/v1beta1/elfcluster_types.go b/api/v1beta1/elfcluster_types.go index 3ad281c0..913425f2 100644 --- a/api/v1beta1/elfcluster_types.go +++ b/api/v1beta1/elfcluster_types.go @@ -18,7 +18,7 @@ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" ) const ( @@ -37,6 +37,11 @@ type ElfClusterSpec struct { // Cluster is a unique identifier for a ELF cluster. Cluster string `json:"cluster,omitempty"` + // StorageCluster defines the storage configuration used for VM disks. + // If unset, the compute cluster's storage is used. + // +optional + StorageCluster StorageConfig `json:"storageCluster,omitempty"` + // ClusterType is the type of the ELF cluster. // If ClusterType is empty, it will be automatically set. // +kubebuilder:validation:Enum=Standard;Stretched diff --git a/api/v1beta1/elfmachine_types.go b/api/v1beta1/elfmachine_types.go index 3d67f3e4..5ffc586f 100644 --- a/api/v1beta1/elfmachine_types.go +++ b/api/v1beta1/elfmachine_types.go @@ -22,9 +22,9 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capierrors "sigs.k8s.io/cluster-api/errors" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" ) const ( @@ -224,6 +224,14 @@ type ElfMachineStatus struct { // ComputeCluster is the status of the compute cluster. // +optional ComputeCluster ComputeClusterStatus `json:"computeCluster,omitempty"` + + // StorageCluster is the status of the storage cluster used by the VM. + // +optional + StorageCluster StorageClusterStatus `json:"storageCluster,omitempty"` + + // DataStore is the status of the datastore used by the VM. + // +optional + DataStore DataStoreStatus `json:"dataStore,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 67772d6d..305f7dae 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -75,7 +75,40 @@ func (z ElfClusterZoneType) ToLower() string { return strings.ToLower(string(z)) } +// StorageConfig defines the target storage for VM disks. +// Exactly one of DatastoreID or StorageClusterID must be specified. +// +kubebuilder:validation:XValidation:rule="has(self.datastoreID) != has(self.storageClusterID)",message="exactly one of datastoreID or storageClusterID must be specified" +type StorageConfig struct { + // DatastoreID is the ID of a VCFS datastore used for VM disks. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + // +optional + DatastoreID string `json:"datastoreID,omitempty"` + + // StorageClusterID is the ID of a clustered storage resource used for VM disks. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + // +optional + StorageClusterID string `json:"storageClusterID,omitempty"` +} + type Tower struct { + TowerClientConfig `json:",inline"` + + // SecretRef is the reference to the secret containing the tower information. + SecretRef *corev1.SecretReference `json:"secretRef,omitempty"` +} + +func (t *Tower) String() string { + if t.SecretRef != nil { + return fmt.Sprintf("%s/%s", t.SecretRef.Namespace, t.SecretRef.Name) + } + + return t.TowerClientConfig.Server +} + +// TowerClientConfig is the connection information for the tower server. +type TowerClientConfig struct { // Server is address of the tower server. Server string `json:"server,omitempty"` @@ -298,6 +331,42 @@ type ComputeClusterStatus struct { Name string `json:"name,omitempty"` } +// StorageClusterStatus is the status of a storage cluster. +type StorageClusterStatus struct { + // ClusterID is the ID of the storage cluster. + ClusterID string `json:"clusterId,omitempty"` + + // Name is the name of the storage cluster. + Name string `json:"name,omitempty"` +} + +// String returns a string representation of this StorageClusterStatus. +func (sc *StorageClusterStatus) String() string { + return fmt.Sprintf("%s:%s", sc.ClusterID, sc.Name) +} + +func (sc *StorageClusterStatus) Equal(other *StorageClusterStatus) bool { + return sc.ClusterID == other.ClusterID && sc.Name == other.Name +} + +// DataStoreStatus is the status of a datastore. +type DataStoreStatus struct { + // DataStoreID is the ID of the datastore. + DataStoreID string `json:"dataStoreId,omitempty"` + + // Name is the name of the datastore. + Name string `json:"name,omitempty"` +} + +// String returns a string representation of this DataStoreStatus. +func (ds *DataStoreStatus) String() string { + return fmt.Sprintf("%s:%s", ds.DataStoreID, ds.Name) +} + +func (ds *DataStoreStatus) Equal(other *DataStoreStatus) bool { + return ds.DataStoreID == other.DataStoreID && ds.Name == other.Name +} + // String returns a string representation of this ComputeClusterStatus. func (cc *ComputeClusterStatus) String() string { return fmt.Sprintf("%s:%s", cc.ClusterID, cc.Name) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 20affe1e..708285dd 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1beta1 import ( "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" - apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + corev1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/cluster-api/errors" ) @@ -58,12 +58,27 @@ func (in *ComputeClusterStatus) DeepCopy() *ComputeClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataStoreStatus) DeepCopyInto(out *DataStoreStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataStoreStatus. +func (in *DataStoreStatus) DeepCopy() *DataStoreStatus { + if in == nil { + return nil + } + out := new(DataStoreStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ElfCluster) DeepCopyInto(out *ElfCluster) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -120,7 +135,8 @@ func (in *ElfClusterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ElfClusterSpec) DeepCopyInto(out *ElfClusterSpec) { *out = *in - out.Tower = in.Tower + out.StorageCluster = in.StorageCluster + in.Tower.DeepCopyInto(&out.Tower) out.ControlPlaneEndpoint = in.ControlPlaneEndpoint } @@ -139,14 +155,14 @@ func (in *ElfClusterStatus) DeepCopyInto(out *ElfClusterStatus) { *out = *in if in.FailureDomains != nil { in, out := &in.FailureDomains, &out.FailureDomains - *out = make(apiv1beta1.FailureDomains, len(*in)) + *out = make(corev1beta1.FailureDomains, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(apiv1beta1.Conditions, len(*in)) + *out = make(corev1beta1.Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -270,14 +286,14 @@ func (in *ElfMachineStatus) DeepCopyInto(out *ElfMachineStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make(apiv1beta1.Conditions, len(*in)) + *out = make(corev1beta1.Conditions, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.Addresses != nil { in, out := &in.Addresses, &out.Addresses - *out = make([]apiv1beta1.MachineAddress, len(*in)) + *out = make([]corev1beta1.MachineAddress, len(*in)) copy(*out, *in) } if in.Network != nil { @@ -305,6 +321,8 @@ func (in *ElfMachineStatus) DeepCopyInto(out *ElfMachineStatus) { } out.Zone = in.Zone out.ComputeCluster = in.ComputeCluster + out.StorageCluster = in.StorageCluster + out.DataStore = in.DataStore } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElfMachineStatus. @@ -552,9 +570,45 @@ func (in *ResourcesStatus) DeepCopy() *ResourcesStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageClusterStatus) DeepCopyInto(out *StorageClusterStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageClusterStatus. +func (in *StorageClusterStatus) DeepCopy() *StorageClusterStatus { + if in == nil { + return nil + } + out := new(StorageClusterStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageConfig) DeepCopyInto(out *StorageConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageConfig. +func (in *StorageConfig) DeepCopy() *StorageConfig { + if in == nil { + return nil + } + out := new(StorageConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Tower) DeepCopyInto(out *Tower) { *out = *in + out.TowerClientConfig = in.TowerClientConfig + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tower. @@ -567,6 +621,21 @@ func (in *Tower) DeepCopy() *Tower { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TowerClientConfig) DeepCopyInto(out *TowerClientConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TowerClientConfig. +func (in *TowerClientConfig) DeepCopy() *TowerClientConfig { + if in == nil { + return nil + } + out := new(TowerClientConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VGPUDeviceSpec) DeepCopyInto(out *VGPUDeviceSpec) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_elfclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_elfclusters.yaml index def65053..4326b2c9 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_elfclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_elfclusters.yaml @@ -84,6 +84,28 @@ spec: - host - port type: object + storageCluster: + description: |- + StorageCluster defines the storage configuration used for VM disks. + If unset, the compute cluster's storage is used. + properties: + datastoreID: + description: DatastoreID is the ID of a VCFS datastore used for + VM disks. + maxLength: 128 + minLength: 1 + type: string + storageClusterID: + description: StorageClusterID is the ID of a clustered storage + resource used for VM disks. + maxLength: 128 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of datastoreID or storageClusterID must be + specified + rule: has(self.datastoreID) != has(self.storageClusterID) tower: description: Tower is the config of tower. properties: @@ -97,6 +119,20 @@ spec: description: Password is the password used to access the tower server. type: string + secretRef: + description: SecretRef is the reference to the secret containing + the tower information. + properties: + name: + description: name is unique within a namespace to reference + a secret resource. + type: string + namespace: + description: namespace defines the space within which the + secret name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic server: description: Server is address of the tower server. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_elfmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_elfmachines.yaml index b7d82f44..1b8fed73 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_elfmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_elfmachines.yaml @@ -527,6 +527,17 @@ spec: - type type: object type: array + dataStore: + description: DataStore is the status of the datastore used by the + VM. + properties: + dataStoreId: + description: DataStoreID is the ID of the datastore. + type: string + name: + description: Name is the name of the datastore. + type: string + type: object failureMessage: description: |- FailureMessage will be set in the event that there is a terminal problem @@ -655,6 +666,17 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + storageCluster: + description: StorageCluster is the status of the storage cluster used + by the VM. + properties: + clusterId: + description: ClusterID is the ID of the storage cluster. + type: string + name: + description: Name is the name of the storage cluster. + type: string + type: object taskRef: description: |- TaskRef is a managed object reference to a Task related to the machine. diff --git a/controllers/elfcluster_controller.go b/controllers/elfcluster_controller.go index 766adbaf..5af9d75b 100644 --- a/controllers/elfcluster_controller.go +++ b/controllers/elfcluster_controller.go @@ -25,11 +25,10 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" - "sigs.k8s.io/cluster-api/util/patch" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -48,6 +47,7 @@ import ( towerresources "github.com/smartxworks/cluster-api-provider-elf/pkg/resources" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" "github.com/smartxworks/cluster-api-provider-elf/pkg/util" + capieutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/capi" machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" ) @@ -115,7 +115,7 @@ func (r *ElfClusterReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (_ } // Fetch the CAPI Cluster. - cluster, err := capiutil.GetOwnerCluster(ctx, r.Client, elfCluster.ObjectMeta) + cluster, err := capieutil.GetOwnerCluster(ctx, r.Client, elfCluster.ObjectMeta) if err != nil { return reconcile.Result{}, err } @@ -127,7 +127,7 @@ func (r *ElfClusterReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (_ log = log.WithValues("Cluster", klog.KObj(cluster)) ctx = ctrl.LoggerInto(ctx, log) - if annotations.IsPaused(cluster, &elfCluster) { + if capieutil.IsPaused(cluster, &elfCluster) { log.V(4).Info("ElfCluster linked to a cluster that is paused") return reconcile.Result{}, nil @@ -149,9 +149,9 @@ func (r *ElfClusterReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (_ // If ElfCluster is being deleting and ForceDeleteCluster flag is set, skip creating the VMService object, // because Tower server may be out of service. So we can force delete ElfCluster. if elfCluster.ObjectMeta.DeletionTimestamp.IsZero() || !elfCluster.HasForceDeleteCluster() { - vmService, err := r.NewVMService(ctx, elfCluster.GetTower(), log) + vmService, err := r.NewVMService(ctx, r.Client, elfCluster.GetTower(), log) if err != nil { - conditions.MarkFalse(&elfCluster, infrav1.TowerAvailableCondition, infrav1.TowerUnreachableReason, clusterv1.ConditionSeverityError, err.Error()) + conditions.MarkFalse(&elfCluster, infrav1.TowerAvailableCondition, infrav1.TowerUnreachableReason, clusterv1.ConditionSeverityError, "%s", err.Error()) return reconcile.Result{}, err } @@ -283,28 +283,28 @@ func (r *ElfClusterReconciler) cleanOrphanLabels(ctx goctx.Context, clusterCtx * log := ctrl.LoggerFrom(ctx) // Locking ensures that only one coroutine cleans at the same time - if ok := acquireLockForGCTowerLabels(clusterCtx.ElfCluster.Spec.Tower.Server); ok { - defer releaseLockForForGCTowerLabels(clusterCtx.ElfCluster.Spec.Tower.Server) + if ok := acquireLockForGCTowerLabels(clusterCtx.ElfCluster.Spec.Tower.String()); ok { + defer releaseLockForForGCTowerLabels(clusterCtx.ElfCluster.Spec.Tower.String()) } else { return } - log.V(1).Info(fmt.Sprintf("Cleaning orphan labels in Tower %s created by CAPE", clusterCtx.ElfCluster.Spec.Tower.Server)) + log.V(1).Info(fmt.Sprintf("Cleaning orphan labels in Tower %s created by CAPE", clusterCtx.ElfCluster.Spec.Tower.String())) keys := []string{towerresources.GetVMLabelClusterName(), towerresources.GetVMLabelVIP(), towerresources.GetVMLabelNamespace()} labelIDs, err := clusterCtx.VMService.CleanUnusedLabels(keys) if err != nil { - log.Error(err, "Warning: failed to clean orphan labels in Tower "+clusterCtx.ElfCluster.Spec.Tower.Server) + log.Error(err, "Warning: failed to clean orphan labels in Tower "+clusterCtx.ElfCluster.Spec.Tower.String()) return } - recordGCTimeForTowerLabels(clusterCtx.ElfCluster.Spec.Tower.Server) + recordGCTimeForTowerLabels(clusterCtx.ElfCluster.Spec.Tower.String()) - log.V(1).Info(fmt.Sprintf("Labels of Tower %s are cleaned successfully", clusterCtx.ElfCluster.Spec.Tower.Server), "labelCount", len(labelIDs)) + log.V(1).Info(fmt.Sprintf("Labels of Tower %s are cleaned successfully", clusterCtx.ElfCluster.Spec.Tower.String()), "labelCount", len(labelIDs)) } -func (r *ElfClusterReconciler) reconcileNormal(ctx goctx.Context, clusterCtx *context.ClusterContext) (reconcile.Result, error) { //nolint:unparam +func (r *ElfClusterReconciler) reconcileNormal(ctx goctx.Context, clusterCtx *context.ClusterContext) (reconcile.Result, error) { log := ctrl.LoggerFrom(ctx) log.Info("Reconciling ElfCluster") @@ -323,7 +323,7 @@ func (r *ElfClusterReconciler) reconcileNormal(ctx goctx.Context, clusterCtx *co if ok, err := r.reconcileFailureDomains(ctx, clusterCtx); err != nil { return reconcile.Result{}, err } else if !ok { - return reconcile.Result{}, nil + return reconcile.Result{RequeueAfter: config.Cape.DefaultRequeueTimeout}, nil } // Reconcile the ElfCluster resource's ready state. diff --git a/controllers/elfcluster_controller_test.go b/controllers/elfcluster_controller_test.go index a03c9ada..a3f3ab6e 100644 --- a/controllers/elfcluster_controller_test.go +++ b/controllers/elfcluster_controller_test.go @@ -32,9 +32,10 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capiutil "sigs.k8s.io/cluster-api/util" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -71,7 +72,7 @@ var _ = Describe("ElfClusterReconciler", func() { // mock mockCtrl = gomock.NewController(GinkgoT()) mockVMService = mock_services.NewMockVMService(mockCtrl) - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, nil } }) @@ -218,7 +219,7 @@ var _ = Describe("ElfClusterReconciler", func() { }) It("should delete failed when tower is out of service", func() { - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, errors.New("get vm service failed") } ctrlMgrCtx := fake.NewControllerManagerContext(elfCluster, cluster) @@ -234,7 +235,7 @@ var _ = Describe("ElfClusterReconciler", func() { }) It("should force delete when tower is out of service and cluster need to force delete", func() { - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, errors.New("get vm service failed") } elfCluster.Annotations = map[string]string{ @@ -275,16 +276,16 @@ var _ = Describe("ElfClusterReconciler", func() { mockVMService.EXPECT().CleanUnusedLabels(keys).Return(nil, unexpectedError) reconciler := &ElfClusterReconciler{ControllerManagerContext: ctrlMgrCtx, NewVMService: mockNewVMService} reconciler.cleanOrphanLabels(ctx, clusterCtx) - Expect(logBuffer.String()).To(ContainSubstring("Warning: failed to clean orphan labels in Tower " + elfCluster.Spec.Tower.Server)) + Expect(logBuffer.String()).To(ContainSubstring("Warning: failed to clean orphan labels in Tower " + elfCluster.Spec.Tower.String())) logBuffer.Reset() mockVMService.EXPECT().CleanUnusedLabels(keys).Return(nil, nil) reconciler.cleanOrphanLabels(ctx, clusterCtx) - Expect(logBuffer.String()).To(ContainSubstring(fmt.Sprintf("Labels of Tower %s are cleaned successfully", elfCluster.Spec.Tower.Server))) + Expect(logBuffer.String()).To(ContainSubstring(fmt.Sprintf("Labels of Tower %s are cleaned successfully", elfCluster.Spec.Tower.String()))) logBuffer.Reset() reconciler.cleanOrphanLabels(ctx, clusterCtx) - Expect(logBuffer.String()).NotTo(ContainSubstring(fmt.Sprintf("Cleaning orphan labels in Tower %s created by CAPE", elfCluster.Spec.Tower.Server))) + Expect(logBuffer.String()).NotTo(ContainSubstring(fmt.Sprintf("Cleaning orphan labels in Tower %s created by CAPE", elfCluster.Spec.Tower.String()))) }) }) diff --git a/controllers/elfmachine_controller.go b/controllers/elfmachine_controller.go index 95ce0ee4..6c92f11e 100644 --- a/controllers/elfmachine_controller.go +++ b/controllers/elfmachine_controller.go @@ -34,13 +34,12 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/klog/v2" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/api/v1beta1/index" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/cluster-api/api/core/v1beta2/index" "sigs.k8s.io/cluster-api/controllers/clustercache" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/conditions" - "sigs.k8s.io/cluster-api/util/patch" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -57,6 +56,7 @@ import ( towerresources "github.com/smartxworks/cluster-api-provider-elf/pkg/resources" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" "github.com/smartxworks/cluster-api-provider-elf/pkg/util" + capieutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/capi" labelsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/labels" machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" patchutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/patch" @@ -132,7 +132,7 @@ func (r *ElfMachineReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (r } // Fetch the CAPI Machine. - machine, err := capiutil.GetOwnerMachine(ctx, r.Client, elfMachine.ObjectMeta) + machine, err := capieutil.GetOwnerMachine(ctx, r.Client, elfMachine.ObjectMeta) if err != nil { return reconcile.Result{}, err } @@ -145,7 +145,7 @@ func (r *ElfMachineReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (r ctx = ctrl.LoggerInto(ctx, log) // Fetch the CAPI Cluster. - cluster, err := capiutil.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) + cluster, err := capieutil.GetClusterFromMetadata(ctx, r.Client, machine.ObjectMeta) if err != nil { log.Info("Machine is missing cluster label or cluster does not exist") @@ -159,7 +159,7 @@ func (r *ElfMachineReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (r log = log.WithValues("Cluster", klog.KObj(cluster)) ctx = ctrl.LoggerInto(ctx, log) - if annotations.IsPaused(cluster, &elfMachine) { + if capieutil.IsPaused(cluster, &elfMachine) { log.V(4).Info("ElfMachine linked to a cluster that is paused") return reconcile.Result{}, nil @@ -201,9 +201,9 @@ func (r *ElfMachineReconciler) Reconcile(ctx goctx.Context, req ctrl.Request) (r // If ElfMachine is being deleting and ElfCLuster ForceDeleteCluster flag is set, skip creating the VMService object, // because Tower server may be out of service. So we can force delete ElfCluster. if elfMachine.ObjectMeta.DeletionTimestamp.IsZero() || !elfCluster.HasForceDeleteCluster() { - vmService, err := r.NewVMService(ctx, elfCluster.GetTower(), log) + vmService, err := r.NewVMService(ctx, r.Client, elfCluster.GetTower(), log) if err != nil { - conditions.MarkFalse(&elfMachine, infrav1.TowerAvailableCondition, infrav1.TowerUnreachableReason, clusterv1.ConditionSeverityError, err.Error()) + conditions.MarkFalse(&elfMachine, infrav1.TowerAvailableCondition, infrav1.TowerUnreachableReason, clusterv1.ConditionSeverityError, "%s", err.Error()) return reconcile.Result{}, err } @@ -390,7 +390,7 @@ func (r *ElfMachineReconciler) reconcileDelete(ctx goctx.Context, machineCtx *co return reconcile.Result{}, nil } - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return reconcile.Result{}, err } @@ -441,11 +441,11 @@ func (r *ElfMachineReconciler) reconcileNormal(ctx goctx.Context, machineCtx *co // Make sure bootstrap data is available and populated. if machineCtx.Machine.Spec.Bootstrap.DataSecretName == nil { if !machineutil.IsControlPlaneMachine(machineCtx.ElfMachine) && !conditions.IsTrue(machineCtx.Cluster, clusterv1.ControlPlaneInitializedCondition) { - log.Info("Waiting for the control plane to be initialized") + log.V(1).Info("Waiting for the control plane to be initialized") conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, clusterv1.WaitingForControlPlaneAvailableReason, clusterv1.ConditionSeverityInfo, "") - return ctrl.Result{}, nil + return ctrl.Result{RequeueAfter: config.Cape.DefaultRequeueTimeout}, nil } log.Info("Waiting for bootstrap data to be available") @@ -538,7 +538,7 @@ func (r *ElfMachineReconciler) reconcileVM(ctx goctx.Context, machineCtx *contex bootstrapData, err := r.getBootstrapData(ctx, machineCtx, hostName) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.CloningFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.CloningFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return nil, false, err } @@ -583,11 +583,12 @@ func (r *ElfMachineReconciler) reconcileVM(ctx goctx.Context, machineCtx *contex log.Info("Create VM for ElfMachine", "hostName", hostName) vmInfo := &service.CloneVMInfo{ - Cluster: machineCtx.GetElfClusterID(), - Host: service.GetTowerString(hostID), - CloudInit: bootstrapData, - GPUDevices: gpuDeviceInfos, - HostName: hostName, + Cluster: machineCtx.GetElfClusterID(), + StorageConfig: machineCtx.GetStorageConfig(), + Host: service.GetTowerString(hostID), + CloudInit: bootstrapData, + GPUDevices: gpuDeviceInfos, + HostName: hostName, } withTaskVM, err := machineCtx.VMService.Clone(machineCtx.ElfCluster, machineCtx.ElfMachine, vmInfo) if err != nil { @@ -609,7 +610,7 @@ func (r *ElfMachineReconciler) reconcileVM(ctx goctx.Context, machineCtx *contex log.Error(err, "failed to create VM", "vmRef", machineCtx.ElfMachine.Status.VMRef, "taskRef", machineCtx.ElfMachine.Status.TaskRef) - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.CloningFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.CloningFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return nil, false, err } @@ -810,7 +811,7 @@ func (r *ElfMachineReconciler) shutDownVM(ctx goctx.Context, machineCtx *context task, err := machineCtx.VMService.ShutDown(machineCtx.ElfMachine.Status.VMRef) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.ShuttingDownFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.ShuttingDownFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return errors.Wrapf(err, "failed to trigger shut down for VM %s", ctx) } @@ -835,7 +836,7 @@ func (r *ElfMachineReconciler) powerOffVM(ctx goctx.Context, machineCtx *context task, err := machineCtx.VMService.PowerOff(machineCtx.ElfMachine.Status.VMRef) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.PoweringOffFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.PoweringOffFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return errors.Wrapf(err, "failed to trigger powering off for VM %s", machineCtx.ElfMachine.Status.VMRef) } @@ -881,7 +882,7 @@ func (r *ElfMachineReconciler) powerOnVM(ctx goctx.Context, machineCtx *context. task, err := machineCtx.VMService.PowerOn(machineCtx.ElfMachine.Status.VMRef, hostID) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.PoweringOnFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.PoweringOnFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return errors.Wrapf(err, "failed to trigger power on for VM %s", machineCtx.ElfMachine.Status.VMRef) } @@ -906,7 +907,7 @@ func (r *ElfMachineReconciler) updateVM(ctx goctx.Context, machineCtx *context.M withTaskVM, err := machineCtx.VMService.UpdateVM(vm, machineCtx.ElfMachine) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.UpdatingFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.UpdatingFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return errors.Wrapf(err, "failed to trigger update for VM %s", ctx) } @@ -1016,7 +1017,7 @@ func (r *ElfMachineReconciler) reconcileVMFailedTask(ctx goctx.Context, machineC if service.IsGPUAssignFailed(errorMessage) { errorMessage = service.ParseGPUAssignFailed(errorMessage) } - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.TaskFailureReason, clusterv1.ConditionSeverityInfo, errorMessage) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.TaskFailureReason, clusterv1.ConditionSeverityInfo, "%s", errorMessage) if service.IsCloudInitConfigError(errorMessage) { machineCtx.ElfMachine.Status.FailureReason = ptr.To(capeerrors.CloudInitConfigError) @@ -1039,12 +1040,12 @@ func (r *ElfMachineReconciler) reconcileVMFailedTask(ctx goctx.Context, machineC case service.IsUpdateVMDiskTask(task, machineCtx.ElfMachine.Name): reason := conditions.GetReason(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition) if reason == infrav1.ExpandingVMDiskReason || reason == infrav1.ExpandingVMDiskFailedReason { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMDiskFailedReason, clusterv1.ConditionSeverityWarning, errorMessage) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMDiskFailedReason, clusterv1.ConditionSeverityWarning, "%s", errorMessage) } case service.IsUpdateVMTask(task) && conditions.IsFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition): reason := conditions.GetReason(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition) if reason == infrav1.ExpandingVMComputeResourcesReason || reason == infrav1.ExpandingVMComputeResourcesFailedReason { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMComputeResourcesFailedReason, clusterv1.ConditionSeverityWarning, errorMessage) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMComputeResourcesFailedReason, clusterv1.ConditionSeverityWarning, "%s", errorMessage) } case service.IsPowerOnVMTask(task) || service.IsUpdateVMTask(task) || service.IsVMColdMigrationTask(task): if machineCtx.ElfMachine.RequiresGPUDevices() { @@ -1103,6 +1104,60 @@ func (r *ElfMachineReconciler) reconcileHostAndZone(ctx goctx.Context, machineCt } } + // Resolve the storage backing the VM's system disk. Tower exposes this + // information on the VM volume rather than on the VM itself. + if machineCtx.GetStorageConfig() != nil && len(vm.VMDisks) > 0 { + vmDiskIDs := make([]string, 0, len(vm.VMDisks)) + for _, vmDisk := range vm.VMDisks { + if vmDisk != nil && vmDisk.ID != nil { + vmDiskIDs = append(vmDiskIDs, *vmDisk.ID) + } + } + + if len(vmDiskIDs) > 0 { + vmDisks, err := machineCtx.VMService.GetVMDisks(vmDiskIDs) + if err != nil { + return errors.Wrapf(err, "failed to get disks for vm %s", service.GetTowerString(vm.ID)) + } + + systemDisk := service.GetVMSystemDisk(vmDisks) + if systemDisk != nil && systemDisk.VMVolume != nil && systemDisk.VMVolume.ID != nil { + vmVolume, err := machineCtx.VMService.GetVMVolume(*systemDisk.VMVolume.ID) + if err != nil { + return errors.Wrapf(err, "failed to get storage volume for vm %s", service.GetTowerString(vm.ID)) + } + if vmVolume == nil { + return errors.Errorf("storage volume %s for vm %s is empty", *systemDisk.VMVolume.ID, service.GetTowerString(vm.ID)) + } + + if vmVolume.StorageClusterConfig != nil { + if storageCluster := vmVolume.StorageClusterConfig.StorageCluster; storageCluster != nil { + storageClusterStatus := infrav1.StorageClusterStatus{ + ClusterID: service.GetTowerString(storageCluster.ID), + Name: service.GetTowerString(storageCluster.Name), + } + if !storageClusterStatus.Equal(&machineCtx.ElfMachine.Status.StorageCluster) { + log.V(1).Info(fmt.Sprintf("Updated VM storage cluster from %s to %s", &machineCtx.ElfMachine.Status.StorageCluster, storageClusterStatus)) + machineCtx.ElfMachine.Status.StorageCluster = storageClusterStatus + } + } + + if datastore := vmVolume.StorageClusterConfig.Datastore; datastore != nil { + dataStoreStatus := infrav1.DataStoreStatus{ + DataStoreID: service.GetTowerString(datastore.ID), + Name: service.GetTowerString(datastore.Name), + } + + if !dataStoreStatus.Equal(&machineCtx.ElfMachine.Status.DataStore) { + log.V(1).Info(fmt.Sprintf("Updated VM datastore from %s to %s", &machineCtx.ElfMachine.Status.DataStore, dataStoreStatus)) + machineCtx.ElfMachine.Status.DataStore = dataStoreStatus + } + } + } + } + } + } + if !machineCtx.ElfCluster.IsStretched() { return nil } @@ -1184,6 +1239,10 @@ func (r *ElfMachineReconciler) reconcileNode(ctx goctx.Context, machineCtx *cont keys := []string{ infrav1.ComputeClusterIDLabel, infrav1.ComputeClusterNameLabel, + infrav1.StorageClusterIDLabel, + infrav1.StorageClusterNameLabel, + infrav1.DataStoreIDLabel, + infrav1.DataStoreNameLabel, infrav1.HostServerIDLabel, infrav1.HostServerNameLabel, infrav1.TowerVMIDLabel, @@ -1212,11 +1271,23 @@ func (r *ElfMachineReconciler) reconcileNode(ctx goctx.Context, machineCtx *cont expectedLabels := map[string]interface{}{ infrav1.ComputeClusterIDLabel: machineCtx.ElfMachine.Status.ComputeCluster.ClusterID, infrav1.ComputeClusterNameLabel: labelsutil.ConvertToLabelValue(machineCtx.ElfMachine.Status.ComputeCluster.Name), + infrav1.StorageClusterIDLabel: nil, + infrav1.StorageClusterNameLabel: nil, + infrav1.DataStoreIDLabel: nil, + infrav1.DataStoreNameLabel: nil, infrav1.HostServerIDLabel: machineCtx.ElfMachine.Status.HostServerRef, infrav1.HostServerNameLabel: labelsutil.ConvertToLabelValue(machineCtx.ElfMachine.Status.HostServerName), infrav1.TowerVMIDLabel: service.GetTowerString(vm.ID), infrav1.NodeGroupLabel: machineutil.GetNodeGroupName(machineCtx.Machine), } + if machineCtx.ElfMachine.Status.DataStore.DataStoreID != "" { + expectedLabels[infrav1.DataStoreIDLabel] = machineCtx.ElfMachine.Status.DataStore.DataStoreID + expectedLabels[infrav1.DataStoreNameLabel] = labelsutil.ConvertToLabelValue(machineCtx.ElfMachine.Status.DataStore.Name) + } + if machineCtx.ElfMachine.Status.StorageCluster.ClusterID != "" { + expectedLabels[infrav1.StorageClusterIDLabel] = machineCtx.ElfMachine.Status.StorageCluster.ClusterID + expectedLabels[infrav1.StorageClusterNameLabel] = labelsutil.ConvertToLabelValue(machineCtx.ElfMachine.Status.StorageCluster.Name) + } if machineCtx.ElfMachine.Status.Zone.Type != "" { expectedLabels[infrav1.ZoneTypeLabel] = machineCtx.ElfMachine.Status.Zone.Type.ToLower() @@ -1285,7 +1356,7 @@ func (r *ElfMachineReconciler) reconcileNetwork(ctx goctx.Context, machineCtx *c defer func() { if reterr != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.WaitingForNetworkAddressesReason, clusterv1.ConditionSeverityWarning, reterr.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.WaitingForNetworkAddressesReason, clusterv1.ConditionSeverityWarning, "%s", reterr.Error()) } else if !ret { log.V(1).Info("VM network is not ready yet", "nicStatus", machineCtx.ElfMachine.Status.Network) conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.WaitingForNetworkAddressesReason, clusterv1.ConditionSeverityInfo, "") diff --git a/controllers/elfmachine_controller_gpu.go b/controllers/elfmachine_controller_gpu.go index 00a952c0..98c62ce4 100644 --- a/controllers/elfmachine_controller_gpu.go +++ b/controllers/elfmachine_controller_gpu.go @@ -23,8 +23,8 @@ import ( "github.com/smartxworks/cloudtower-go-sdk/v2/models" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" ctrl "sigs.k8s.io/controller-runtime" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -56,7 +56,7 @@ func (r *ElfMachineReconciler) selectHostAndGPUsForVM(ctx goctx.Context, machine var availableHosts service.Hosts defer func() { if reterr != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.SelectingGPUFailedReason, clusterv1.ConditionSeverityError, reterr.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.SelectingGPUFailedReason, clusterv1.ConditionSeverityError, "%s", reterr.Error()) } else if rethost == nil { if availableHosts.Len() == 0 { conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.WaitingForELFClusterWithSufficientMemoryReason, clusterv1.ConditionSeverityWarning, "") @@ -317,7 +317,7 @@ func (r *ElfMachineReconciler) addGPUDevicesForVM(ctx goctx.Context, machineCtx task, err := machineCtx.VMService.AddGPUDevices(machineCtx.ElfMachine.Status.VMRef, gpuDeviceInfos) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.AttachingGPUFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.AttachingGPUFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) unlockGPUDevicesLockedByVM(machineCtx.GetElfClusterID(), machineCtx.ElfMachine.Name) @@ -365,7 +365,7 @@ func (r *ElfMachineReconciler) removeVMGPUDevices(ctx goctx.Context, machineCtx // the original error message will not be overwritten if the remove fails. message := conditions.GetMessage(machineCtx.ElfMachine, infrav1.VMProvisionedCondition) if !(service.IsGPUAssignFailed(message) || service.IsVGPUInsufficientError(message)) { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.DetachingGPUFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.DetachingGPUFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) } return errors.Wrapf(err, "failed to trigger detaching stale GPU devices for VM %s", machineCtx.ElfMachine.Status.VMRef) diff --git a/controllers/elfmachine_controller_gpu_test.go b/controllers/elfmachine_controller_gpu_test.go index e7e1ee01..8357895b 100644 --- a/controllers/elfmachine_controller_gpu_test.go +++ b/controllers/elfmachine_controller_gpu_test.go @@ -28,8 +28,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -73,7 +73,7 @@ var _ = Describe("ElfMachineReconciler-GPU", func() { // mock mockCtrl = gomock.NewController(GinkgoT()) mockVMService = mock_services.NewMockVMService(mockCtrl) - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, nil } }) diff --git a/controllers/elfmachine_controller_placement_group.go b/controllers/elfmachine_controller_placement_group.go index 2297f00f..94141405 100644 --- a/controllers/elfmachine_controller_placement_group.go +++ b/controllers/elfmachine_controller_placement_group.go @@ -27,13 +27,11 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" - capiutil "sigs.k8s.io/cluster-api/util" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/cluster-api/util/annotations" - "sigs.k8s.io/cluster-api/util/collections" - "sigs.k8s.io/cluster-api/util/conditions" - "sigs.k8s.io/cluster-api/util/patch" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -43,6 +41,8 @@ import ( towerresources "github.com/smartxworks/cluster-api-provider-elf/pkg/resources" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" + capieutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/capi" + capiecollections "github.com/smartxworks/cluster-api-provider-elf/pkg/util/capi/collections" kcputil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/kcp" machineutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/machine" typesutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/types" @@ -281,7 +281,7 @@ func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx goctx.Context, mach vmMap := make(map[string]string) for i := range len(placementGroup.Vms) { if elfMachine, ok := elfMachineMap[*placementGroup.Vms[i].Name]; ok { - machine, err := capiutil.GetOwnerMachine(ctx, r.Client, elfMachine.ObjectMeta) + machine, err := capieutil.GetOwnerMachine(ctx, r.Client, elfMachine.ObjectMeta) if err != nil { return "", err } @@ -291,7 +291,7 @@ func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx goctx.Context, mach } } - machines := collections.FromMachines(placementGroupMachines...) + machines := capiecollections.FromMachines(placementGroupMachines...) newestMachine := machines.Newest() if newestMachine == nil { log.Info("Newest machine not found, skip selecting host for VM", "vmRef", machineCtx.ElfMachine.Status.VMRef) @@ -401,7 +401,7 @@ func (r *ElfMachineReconciler) joinPlacementGroup(ctx goctx.Context, machineCtx defer func() { if reterr != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.JoiningPlacementGroupFailedReason, clusterv1.ConditionSeverityWarning, reterr.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.JoiningPlacementGroupFailedReason, clusterv1.ConditionSeverityWarning, "%s", reterr.Error()) } else if !ret { conditions.MarkFalse(machineCtx.ElfMachine, infrav1.VMProvisionedCondition, infrav1.JoiningPlacementGroupReason, clusterv1.ConditionSeverityInfo, "") } diff --git a/controllers/elfmachine_controller_resources.go b/controllers/elfmachine_controller_resources.go index b3bad301..f2fda16d 100644 --- a/controllers/elfmachine_controller_resources.go +++ b/controllers/elfmachine_controller_resources.go @@ -25,9 +25,9 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capiremote "sigs.k8s.io/cluster-api/controllers/remote" - "sigs.k8s.io/cluster-api/util/conditions" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -139,7 +139,7 @@ func (r *ElfMachineReconciler) resizeVMVolume(ctx goctx.Context, machineCtx *con withTaskVMVolume, err := machineCtx.VMService.ResizeVMVolume(*vmVolume.ID, diskSize) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, conditionType, infrav1.ExpandingVMDiskFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, conditionType, infrav1.ExpandingVMDiskFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return errors.Wrapf(err, "failed to trigger expand size from %d to %d for vm volume %s/%s", *vmVolume.Size, diskSize, *vmVolume.ID, *vmVolume.Name) } @@ -211,7 +211,7 @@ func (r *ElfMachineReconciler) reconcileVMCPUAndMemory(ctx goctx.Context, machin withTaskVM, err := machineCtx.VMService.UpdateVM(vm, machineCtx.ElfMachine) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMComputeResourcesFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.ExpandingVMComputeResourcesFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return false, errors.Wrapf(err, "failed to trigger update CPU and memory for VM %s", *vm.Name) } @@ -264,25 +264,25 @@ func (r *ElfMachineReconciler) reconcileHostJob(ctx goctx.Context, machineCtx *c kubeClient, err := capiremote.NewClusterClient(ctx, "", r.Client, client.ObjectKey{Namespace: machineCtx.Cluster.Namespace, Name: machineCtx.Cluster.Name}) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to create kubeClient: "+err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to create kubeClient: %s", err.Error()) return false, err } agentJob, err := hostagent.GetHostJob(ctx, kubeClient, machineCtx.ElfMachine.Namespace, hostagent.GetJobName(machineCtx.ElfMachine, jobType, macTypes)) if err != nil && !apierrors.IsNotFound(err) { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to get HostOperationJob: "+err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to get HostOperationJob: %s", err.Error()) return false, err } if agentJob == nil { agentJob, err = hostagent.GenerateJob(ctx, r.Client, machineCtx.ElfMachine, jobType, macTypes, nodeName) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to generate host agent job: "+err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "failed to generate host agent job: %s", err.Error()) return false, err } if err = kubeClient.Create(ctx, agentJob); err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return false, err } @@ -295,7 +295,7 @@ func (r *ElfMachineReconciler) reconcileHostJob(ctx goctx.Context, machineCtx *c case agentv1.PhaseSucceeded: log.Info("HostJob succeeded", "hostAgentJob", agentJob.Name) case agentv1.PhaseFailed: - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, agentJob.Status.FailureMessage) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, failReason, clusterv1.ConditionSeverityWarning, "%s", agentJob.Status.FailureMessage) log.Info("HostJob failed, will try again after three minutes", "hostAgentJob", agentJob.Name, "failureMessage", agentJob.Status.FailureMessage) lastExecutionTime := agentJob.Status.LastExecutionTime @@ -363,7 +363,7 @@ func (r *ElfMachineReconciler) reconcieVMNetworkDevices(ctx goctx.Context, machi if service.GetTowerString(vmNics[i].IPAddress) == "" { message := fmt.Sprintf("waiting for the vm network device %d ready", i) - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.WaitingForNetworkAddressesReason, clusterv1.ConditionSeverityInfo, message) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.WaitingForNetworkAddressesReason, clusterv1.ConditionSeverityInfo, "%s", message) log.V(1).Info(message) return false, nil @@ -432,7 +432,7 @@ func (r *ElfMachineReconciler) reconcileVMNics(ctx goctx.Context, machineCtx *co withTaskVM, err := machineCtx.VMService.AddVMNics(*vm.ID, newNics) if err != nil { - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.AddingVMNetworkDeviceFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.AddingVMNetworkDeviceFailedReason, clusterv1.ConditionSeverityWarning, "%s", err.Error()) return false, errors.Wrapf(err, "failed to trigger add new nics to vm") } @@ -478,7 +478,7 @@ func (r *ElfMachineReconciler) setVMNetworkDeviceConfig(ctx goctx.Context, machi return true, nil } else if deviceCount != vmNicCount { message := "The expected number of network devices is not equal to the number of vm nics" - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.SettingVMNetworkDeviceConfigFailedReason, clusterv1.ConditionSeverityWarning, message) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.SettingVMNetworkDeviceConfigFailedReason, clusterv1.ConditionSeverityWarning, "%s", message) log.V(1).Info(message, "expectedDevices", machineCtx.ElfMachine.Spec.Network.Devices, "currentNics", formatNics(vmNics)) return false, nil @@ -490,7 +490,7 @@ func (r *ElfMachineReconciler) setVMNetworkDeviceConfig(ctx goctx.Context, machi for i := len(nics) - toConfigNicCount; i >= 0 && i < len(nics); i++ { if service.GetTowerString(nics[i].MacAddress) == "" { message := fmt.Sprintf("Waiting for the vm network device %d mac address", i+1) - conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.WaitingForNetworkMacAddressReason, clusterv1.ConditionSeverityInfo, message) + conditions.MarkFalse(machineCtx.ElfMachine, infrav1.ResourcesHotUpdatedCondition, infrav1.WaitingForNetworkMacAddressReason, clusterv1.ConditionSeverityInfo, "%s", message) log.V(1).Info(message) return false, nil diff --git a/controllers/elfmachine_controller_resources_test.go b/controllers/elfmachine_controller_resources_test.go index ac1c3354..b637d15e 100644 --- a/controllers/elfmachine_controller_resources_test.go +++ b/controllers/elfmachine_controller_resources_test.go @@ -31,8 +31,8 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/klog/v2" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -72,7 +72,7 @@ var _ = Describe("ElfMachineReconciler", func() { // mock mockCtrl = gomock.NewController(GinkgoT()) mockVMService = mock_services.NewMockVMService(mockCtrl) - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, nil } }) diff --git a/controllers/elfmachine_controller_test.go b/controllers/elfmachine_controller_test.go index ebe88ad9..6e8433d5 100644 --- a/controllers/elfmachine_controller_test.go +++ b/controllers/elfmachine_controller_test.go @@ -35,12 +35,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capierrors "sigs.k8s.io/cluster-api/errors" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/conditions" - "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -93,7 +93,7 @@ var _ = Describe("ElfMachineReconciler", func() { // mock mockCtrl = gomock.NewController(GinkgoT()) mockVMService = mock_services.NewMockVMService(mockCtrl) - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, nil } @@ -204,8 +204,9 @@ var _ = Describe("ElfMachineReconciler", func() { reconciler := &ElfMachineReconciler{ControllerManagerContext: ctrlMgrCtx, NewVMService: mockNewVMService} elfMachineKey := capiutil.ObjectKey(elfMachine) - _, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: elfMachineKey}) + result, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: elfMachineKey}) Expect(err).ToNot(HaveOccurred()) + Expect(result.RequeueAfter).To(Equal(config.Cape.DefaultRequeueTimeout)) Expect(logBuffer.String()).To(ContainSubstring("Waiting for the control plane to be initialized")) elfMachine = &infrav1.ElfMachine{} Expect(reconciler.Client.Get(ctx, elfMachineKey, elfMachine)).To(Succeed()) @@ -335,6 +336,8 @@ var _ = Describe("ElfMachineReconciler", func() { vm := fake.NewTowerVM() vm.Name = &elfMachine.Name elfCluster.Spec.Cluster = clusterInsufficientStorageKey + storageConfig := &models.StorageConfig{StorageClusterID: service.TowerString(storageClusterKey)} + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{StorageClusterID: storageClusterKey} task := fake.NewTowerTask("") withTaskVM := fake.NewWithTaskVM(vm, task) ctrlutil.AddFinalizer(elfMachine, infrav1.MachineFinalizer) @@ -367,7 +370,12 @@ var _ = Describe("ElfMachineReconciler", func() { logBuffer = new(bytes.Buffer) klog.SetOutput(logBuffer) - mockVMService.EXPECT().Clone(gomock.Any(), gomock.Any(), gomock.Any()).Return(withTaskVM, nil) + mockVMService.EXPECT().Clone(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ *infrav1.ElfCluster, _ *infrav1.ElfMachine, vmInfo *service.CloneVMInfo) (*models.WithTaskVM, error) { + Expect(vmInfo.StorageConfig).To(Equal(storageConfig)) + + return withTaskVM, nil + }) mockVMService.EXPECT().Get(*vm.ID).Return(vm, nil) mockVMService.EXPECT().GetTask(*task.ID).Return(task, nil) mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) @@ -1023,6 +1031,88 @@ var _ = Describe("ElfMachineReconciler", func() { Expect(elfMachine.Status.HostServerName).To(Equal(*host.Name)) }) + It("should update storage cluster status from the VM volume", func() { + elfCluster.Spec.ClusterType = infrav1.ElfClusterTypeStandard + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{StorageClusterID: fake.ID()} + storageClusterID := fake.ID() + storageClusterName := "storage-cluster-a" + vmVolume := fake.NewVMVolume(elfMachine) + vmVolume.StorageClusterConfig = &models.NestedStorageClusterConfig{ + StorageCluster: &models.NestedCluster{ + ID: ptr.To(storageClusterID), + Name: ptr.To(storageClusterName), + }, + } + vmDisk := fake.NewVMDisk(vmVolume) + vm.VMDisks = []*models.NestedVMDisk{{ID: vmDisk.ID}} + mockVMService.EXPECT().GetVMDisks([]string{*vmDisk.ID}).Return([]*models.VMDisk{vmDisk}, nil) + mockVMService.EXPECT().GetVMVolume(*vmVolume.ID).Return(vmVolume, nil) + + machineCtx := &context.MachineContext{ + ElfCluster: elfCluster, + ElfMachine: elfMachine, + VMService: mockVMService, + } + reconciler := &ElfMachineReconciler{} + err := reconciler.reconcileHostAndZone(ctx, machineCtx, vm) + Expect(err).NotTo(HaveOccurred()) + Expect(elfMachine.Status.StorageCluster).To(Equal(infrav1.StorageClusterStatus{ + ClusterID: storageClusterID, + Name: storageClusterName, + })) + Expect(elfMachine.Status.DataStore).To(Equal(infrav1.DataStoreStatus{})) + }) + + It("should update datastore status from the VM volume", func() { + elfCluster.Spec.ClusterType = infrav1.ElfClusterTypeStandard + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{DatastoreID: fake.ID()} + dataStoreID := fake.ID() + dataStoreName := "datastore-a" + vmVolume := fake.NewVMVolume(elfMachine) + vmVolume.StorageClusterConfig = &models.NestedStorageClusterConfig{ + Datastore: &models.NestedDatastore{ + ID: ptr.To(dataStoreID), + Name: ptr.To(dataStoreName), + }, + } + vmDisk := fake.NewVMDisk(vmVolume) + vm.VMDisks = []*models.NestedVMDisk{{ID: vmDisk.ID}} + mockVMService.EXPECT().GetVMDisks([]string{*vmDisk.ID}).Return([]*models.VMDisk{vmDisk}, nil) + mockVMService.EXPECT().GetVMVolume(*vmVolume.ID).Return(vmVolume, nil) + + machineCtx := &context.MachineContext{ + ElfCluster: elfCluster, + ElfMachine: elfMachine, + VMService: mockVMService, + } + reconciler := &ElfMachineReconciler{} + err := reconciler.reconcileHostAndZone(ctx, machineCtx, vm) + Expect(err).NotTo(HaveOccurred()) + Expect(elfMachine.Status.StorageCluster).To(Equal(infrav1.StorageClusterStatus{})) + Expect(elfMachine.Status.DataStore).To(Equal(infrav1.DataStoreStatus{ + DataStoreID: dataStoreID, + Name: dataStoreName, + })) + }) + + It("should return an error when storage disks cannot be read", func() { + elfCluster.Spec.ClusterType = infrav1.ElfClusterTypeStandard + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{StorageClusterID: fake.ID()} + vmDiskID := fake.ID() + vm.VMDisks = []*models.NestedVMDisk{{ID: ptr.To(vmDiskID)}} + mockVMService.EXPECT().GetVMDisks([]string{vmDiskID}).Return(nil, errors.New("failed to get disks")) + + machineCtx := &context.MachineContext{ + ElfCluster: elfCluster, + ElfMachine: elfMachine, + VMService: mockVMService, + } + reconciler := &ElfMachineReconciler{} + err := reconciler.reconcileHostAndZone(ctx, machineCtx, vm) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get disks for vm")) + }) + It("should update zone status for stretched cluster", func() { elfCluster.Spec.ClusterType = infrav1.ElfClusterTypeStretched zone := fake.NewTowerPreferredZone() @@ -2537,7 +2627,7 @@ var _ = Describe("ElfMachineReconciler", func() { }) It("should delete ElfMachine when tower is out of service and cluster need to force delete", func() { - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, errors.New("get vm service failed") } elfCluster.Annotations = map[string]string{ @@ -2558,7 +2648,7 @@ var _ = Describe("ElfMachineReconciler", func() { }) It("should delete ElfMachine failed when tower is out of service", func() { - mockNewVMService = func(_ goctx.Context, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { + mockNewVMService = func(_ goctx.Context, _ client.Client, _ infrav1.Tower, _ logr.Logger) (service.VMService, error) { return mockVMService, errors.New("get vm service failed") } ctrlMgrCtx := fake.NewControllerManagerContext(elfCluster, cluster, elfMachine, machine, secret, md) @@ -3686,10 +3776,15 @@ var _ = Describe("ElfMachineReconciler", func() { It("should set providerID and labels for node", func() { elfMachine.Spec.GPUDevices = []infrav1.GPUPassthroughDeviceSpec{{Model: "H100"}} + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{StorageClusterID: "storage-cluster-id"} elfMachine.Status.ComputeCluster = infrav1.ComputeClusterStatus{ ClusterID: fake.UUID(), Name: fake.UUID(), } + elfMachine.Status.StorageCluster = infrav1.StorageClusterStatus{ + ClusterID: "observed-storage-cluster-id", + Name: "observed-storage-cluster-name", + } elfMachine.Status.HostServerRef = fake.UUID() elfMachine.Status.HostServerName = fake.UUID() elfMachine.Status.Zone = infrav1.ZoneStatus{ @@ -3739,6 +3834,8 @@ var _ = Describe("ElfMachineReconciler", func() { return node.Spec.ProviderID == providerID && node.Labels[infrav1.ComputeClusterIDLabel] == elfMachine.Status.ComputeCluster.ClusterID && node.Labels[infrav1.ComputeClusterNameLabel] == elfMachine.Status.ComputeCluster.Name && + node.Labels[infrav1.StorageClusterIDLabel] == elfMachine.Status.StorageCluster.ClusterID && + node.Labels[infrav1.StorageClusterNameLabel] == labelsutil.ConvertToLabelValue(elfMachine.Status.StorageCluster.Name) && node.Labels[infrav1.HostServerIDLabel] == elfMachine.Status.HostServerRef && node.Labels[infrav1.HostServerNameLabel] == labelsutil.ConvertToLabelValue(elfMachine.Status.HostServerName) && node.Labels[infrav1.ZoneIDLabel] == elfMachine.Status.Zone.ZoneID && @@ -3751,6 +3848,7 @@ var _ = Describe("ElfMachineReconciler", func() { It("should update labels but not update providerID", func() { elfMachine.Spec.VGPUDevices = []infrav1.VGPUDeviceSpec{{Type: "H300"}} + elfCluster.Spec.StorageCluster = infrav1.StorageConfig{DatastoreID: "datastore-id"} elfMachine.Status.ComputeCluster = infrav1.ComputeClusterStatus{ ClusterID: fake.UUID(), Name: fake.UUID(), @@ -3761,6 +3859,10 @@ var _ = Describe("ElfMachineReconciler", func() { ZoneID: fake.UUID(), Type: infrav1.ElfClusterZoneTypePreferred, } + elfMachine.Status.DataStore = infrav1.DataStoreStatus{ + DataStoreID: "observed-datastore-id", + Name: "observed-datastore-name", + } vm := fake.NewTowerVM() providerID := machineutil.ConvertUUIDToProviderID(*vm.LocalID) ctrlMgrCtx := &context.ControllerManagerContext{ @@ -3782,6 +3884,10 @@ var _ = Describe("ElfMachineReconciler", func() { Labels: map[string]string{ infrav1.ComputeClusterIDLabel: "old-cluster-id", infrav1.ComputeClusterNameLabel: "old-cluster-name", + infrav1.StorageClusterIDLabel: "old-storage-cluster-id", + infrav1.StorageClusterNameLabel: "old-storage-cluster-name", + infrav1.DataStoreIDLabel: "old-datastore-id", + infrav1.DataStoreNameLabel: "old-datastore-name", infrav1.HostServerIDLabel: "old-host-id", infrav1.HostServerNameLabel: "old-host-name", infrav1.ZoneIDLabel: "old-zone-id", @@ -3815,6 +3921,8 @@ var _ = Describe("ElfMachineReconciler", func() { return node.Spec.ProviderID == providerID && node.Labels[infrav1.ComputeClusterIDLabel] == elfMachine.Status.ComputeCluster.ClusterID && node.Labels[infrav1.ComputeClusterNameLabel] == elfMachine.Status.ComputeCluster.Name && + node.Labels[infrav1.DataStoreIDLabel] == elfMachine.Status.DataStore.DataStoreID && + node.Labels[infrav1.DataStoreNameLabel] == labelsutil.ConvertToLabelValue(elfMachine.Status.DataStore.Name) && node.Labels[infrav1.HostServerIDLabel] == elfMachine.Status.HostServerRef && node.Labels[infrav1.HostServerNameLabel] == labelsutil.ConvertToLabelValue(elfMachine.Status.HostServerName) && node.Labels[infrav1.ZoneIDLabel] == elfMachine.Status.Zone.ZoneID && @@ -3850,6 +3958,10 @@ var _ = Describe("ElfMachineReconciler", func() { Labels: map[string]string{ infrav1.ComputeClusterIDLabel: "old-cluster-id", infrav1.ComputeClusterNameLabel: "old-cluster-name", + infrav1.StorageClusterIDLabel: "old-storage-cluster-id", + infrav1.StorageClusterNameLabel: "old-storage-cluster-name", + infrav1.DataStoreIDLabel: "old-datastore-id", + infrav1.DataStoreNameLabel: "old-datastore-name", infrav1.HostServerIDLabel: elfMachine.Status.HostServerRef, infrav1.HostServerNameLabel: elfMachine.Status.HostServerName, infrav1.TowerVMIDLabel: *vm.ID, @@ -3887,6 +3999,10 @@ var _ = Describe("ElfMachineReconciler", func() { node.Labels[infrav1.TowerVMIDLabel] == *vm.ID && node.Labels[infrav1.ComputeClusterIDLabel] == "" && node.Labels[infrav1.ComputeClusterNameLabel] == "" && + !labelsutil.HasLabel(node, infrav1.StorageClusterIDLabel) && + !labelsutil.HasLabel(node, infrav1.StorageClusterNameLabel) && + !labelsutil.HasLabel(node, infrav1.DataStoreIDLabel) && + !labelsutil.HasLabel(node, infrav1.DataStoreNameLabel) && !labelsutil.HasLabel(node, infrav1.ZoneIDLabel) && !labelsutil.HasLabel(node, infrav1.ZoneTypeLabel) && !labelsutil.HasLabel(node, labelsutil.ClusterAutoscalerCAPIGPULabel) @@ -4115,7 +4231,7 @@ var _ = Describe("ElfMachineReconciler", func() { }) }) -func waitStaticIPAllocationSpec(mockNewVMService func(ctx goctx.Context, auth infrav1.Tower, logger logr.Logger) (service.VMService, error), +func waitStaticIPAllocationSpec(mockNewVMService service.NewVMServiceFunc, elfCluster *infrav1.ElfCluster, cluster *clusterv1.Cluster, elfMachine *infrav1.ElfMachine, machine *clusterv1.Machine, secret *corev1.Secret, md *clusterv1.MachineDeployment) { ctrlMgrCtx := fake.NewControllerManagerContext(elfCluster, cluster, elfMachine, machine, secret, md) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 69a9df11..bbde023b 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -31,10 +31,11 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" cgscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/controllers/clustercache" "sigs.k8s.io/cluster-api/controllers/remote" - "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -93,6 +94,7 @@ func setup() { utilruntime.Must(infrav1.AddToScheme(cgscheme.Scheme)) utilruntime.Must(clusterv1.AddToScheme(cgscheme.Scheme)) + utilruntime.Must(clusterv2.AddToScheme(cgscheme.Scheme)) testEnv = helpers.NewTestEnvironment(ctx) @@ -106,6 +108,7 @@ func setup() { } }() <-testEnv.Manager.Elected() + testEnv.WaitForWebhooks(ctx) // create manager pod namespace ns := &corev1.Namespace{ diff --git a/controllers/tower_cache.go b/controllers/tower_cache.go index 252dd082..d3c904a2 100644 --- a/controllers/tower_cache.go +++ b/controllers/tower_cache.go @@ -24,8 +24,8 @@ import ( "github.com/smartxworks/cloudtower-go-sdk/v2/models" "k8s.io/apimachinery/pkg/api/resource" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" diff --git a/controllers/tower_cache_test.go b/controllers/tower_cache_test.go index d4dc2351..30c38cd6 100644 --- a/controllers/tower_cache_test.go +++ b/controllers/tower_cache_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/gomega" "github.com/smartxworks/cloudtower-go-sdk/v2/models" corev1 "k8s.io/api/core/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -39,6 +39,7 @@ const ( clusterKey = "clusterID" clusterInsufficientMemoryKey = "clusterInsufficientMemory" clusterInsufficientStorageKey = "clusterInsufficientStorage" + storageClusterKey = "storageClusterID" placementGroupKey = "getPlacementGroupName" ) diff --git a/go.mod b/go.mod index df6df1c5..2ed887a4 100644 --- a/go.mod +++ b/go.mod @@ -1,45 +1,52 @@ module github.com/smartxworks/cluster-api-provider-elf -go 1.23.11 +go 1.25.9 require ( + github.com/blang/semver/v4 v4.0.0 github.com/caarlos0/env/v11 v11.3.1 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.36.3 + github.com/onsi/gomega v1.38.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 - github.com/smartxworks/cloudtower-go-sdk/v2 v2.21.0-rc.2 + github.com/smartxworks/cloudtower-go-sdk/v2 v2.24.0-rc.9 github.com/smartxworks/host-config-agent-api v0.0.0-20240410021405-be1517a07889 - golang.org/x/mod v0.24.0 - golang.org/x/tools v0.31.0 - k8s.io/api v0.32.3 - k8s.io/apiextensions-apiserver v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/apiserver v0.32.3 - k8s.io/client-go v0.32.3 + golang.org/x/mod v0.31.0 + golang.org/x/sync v0.19.0 + golang.org/x/tools v0.40.0 + k8s.io/api v0.33.3 + k8s.io/apiextensions-apiserver v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/apiserver v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 - sigs.k8s.io/cluster-api v1.10.6 - sigs.k8s.io/cluster-api/test v1.10.6 - sigs.k8s.io/controller-runtime v0.20.4 + sigs.k8s.io/cluster-api v1.11.11 + sigs.k8s.io/cluster-api/test v1.11.11 + sigs.k8s.io/controller-runtime v0.21.0 ) require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect - cel.dev/expr v0.18.0 // indirect + cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/adrg/xdg v0.5.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/coredns/caddy v1.1.1 // indirect + github.com/coredns/corefile-migration v1.0.30 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -47,53 +54,59 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/sdk v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.12.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect - google.golang.org/grpc v1.67.3 // indirect + golang.org/x/net v0.49.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.79.3 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect ) require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.3.0 - github.com/Masterminds/sprig/v3 v3.3.0 // indirect - github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/caitlinelfring/go-env-default v1.1.0 github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/docker v28.0.2+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -110,59 +123,52 @@ require ( github.com/go-openapi/validate v0.22.1 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.22.0 // indirect + github.com/google/cel-go v0.23.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/huandu/xstrings v1.5.0 // 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/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 - github.com/spf13/viper v1.20.0 // indirect + github.com/spf13/pflag v1.0.7 + github.com/spf13/viper v1.20.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/valyala/fastjson v1.6.4 // indirect go.mongodb.org/mongo-driver v1.12.1 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.11.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/protobuf v1.36.11 // 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/cluster-bootstrap v0.32.3 // indirect - k8s.io/component-base v0.32.3 - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + gopkg.in/yaml.v3 v3.0.1 + k8s.io/cluster-bootstrap v0.33.3 // indirect + k8s.io/component-base v0.33.3 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/kind v0.27.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/kind v0.30.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index a2d16013..0c15ed02 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -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 v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -17,8 +15,8 @@ github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+ github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= @@ -41,22 +39,24 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5m github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/caitlinelfring/go-env-default v1.1.0 h1:bhDfXmUolvcIGfQCX8qevQX8wxC54NGz0aimoUnhvDM= github.com/caitlinelfring/go-env-default v1.1.0/go.mod h1:tESXPr8zFPP/cRy3cwxrHBmjJIf2A1x/o4C9CET2rEk= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.27 h1:WIIw5sU0LfGgoGnhdrYdVcto/aWmJoGA/C62iwkU0JM= -github.com/coredns/corefile-migration v1.0.27/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= +github.com/coredns/corefile-migration v1.0.30 h1:ljZNPGgna+4yKv81gfkvkgLEWdtz0NjBR1glaiPI140= +github.com/coredns/corefile-migration v1.0.30/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= @@ -70,18 +70,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= -github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -90,6 +86,7 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -101,8 +98,8 @@ github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXE github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -185,8 +182,6 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -204,10 +199,10 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -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/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -228,24 +223,16 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= -github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= 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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -254,8 +241,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= 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= @@ -265,6 +250,8 @@ github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0Lh github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -275,6 +262,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -300,16 +289,16 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -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/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -356,12 +345,12 @@ github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+q github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/openlyinc/pointy v1.2.0/go.mod h1:JodZOTJoBNaAQHeU0F/SwA4PL0lg4pKF7fYFpX291P0= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -377,17 +366,16 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -397,8 +385,8 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= @@ -407,15 +395,10 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartxworks/cloudtower-go-sdk/v2 v2.21.0-rc.2 h1:jM/uytG4DXkkgfJ9FlGS7/BaYE8IyDvj/73TMoemwmc= -github.com/smartxworks/cloudtower-go-sdk/v2 v2.21.0-rc.2/go.mod h1:X6R9+L438SMnLJXykSCV3fJ+AZul0hlyjITsZgrSRtM= +github.com/smartxworks/cloudtower-go-sdk/v2 v2.24.0-rc.9 h1:/47oOkCXjMDSDv3kJq7pJIuGkaOBaplecPkzXvTMfKI= +github.com/smartxworks/cloudtower-go-sdk/v2 v2.24.0-rc.9/go.mod h1:X6R9+L438SMnLJXykSCV3fJ+AZul0hlyjITsZgrSRtM= github.com/smartxworks/host-config-agent-api v0.0.0-20240410021405-be1517a07889 h1:kXlP3d9BEE6q31cR3qAehK0BKNVuCZ6jT2t+Jf5Gwrs= github.com/smartxworks/host-config-agent-api v0.0.0-20240410021405-be1517a07889/go.mod h1:oexROTOhF0Ium/iTwRbwXwZy+eGkzYE5a5qe3HvvKug= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= 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/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -426,16 +409,19 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= -github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -447,16 +433,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= 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/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -466,62 +448,52 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.20 h1:aKfz3nPZECWoZJXMSH9y6h2adXjtOHaHTGEVCuCmaz0= -go.etcd.io/etcd/api/v3 v3.5.20/go.mod h1:QqKGViq4KTgOG43dr/uH0vmGWIaoJY3ggFi6ZH0TH/U= -go.etcd.io/etcd/client/pkg/v3 v3.5.20 h1:sZIAtra+xCo56gdf6BR62to/hiie5Bwl7hQIqMzVTEM= -go.etcd.io/etcd/client/pkg/v3 v3.5.20/go.mod h1:qaOi1k4ZA9lVLejXNvyPABrVEe7VymMF2433yyRQ7O0= -go.etcd.io/etcd/client/v2 v2.305.16 h1:kQrn9o5czVNaukf2A2At43cE9ZtWauOtf9vRZuiKXow= -go.etcd.io/etcd/client/v2 v2.305.16/go.mod h1:h9YxWCzcdvZENbfzBTFCnoNumr2ax3F19sKMqHFmXHE= -go.etcd.io/etcd/client/v3 v3.5.20 h1:jMT2MwQEhyvhQg49Cec+1ZHJzfUf6ZgcmV0GjPv0tIQ= -go.etcd.io/etcd/client/v3 v3.5.20/go.mod h1:J5lbzYRMUR20YolS5UjlqqMcu3/wdEvG5VNBhzyo3m0= -go.etcd.io/etcd/pkg/v3 v3.5.16 h1:cnavs5WSPWeK4TYwPYfmcr3Joz9BH+TZ6qoUtz6/+mc= -go.etcd.io/etcd/pkg/v3 v3.5.16/go.mod h1:+lutCZHG5MBBFI/U4eYT5yL7sJfnexsoM20Y0t2uNuY= -go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= -go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= -go.etcd.io/etcd/server/v3 v3.5.16 h1:d0/SAdJ3vVsZvF8IFVb1k8zqMZ+heGcNfft71ul9GWE= -go.etcd.io/etcd/server/v3 v3.5.16/go.mod h1:ynhyZZpdDp1Gq49jkUg5mfkDWZwXnn3eIqCqtJnrD/s= +go.etcd.io/etcd/api/v3 v3.5.22 h1:jRqZlcmndfKs1fO9I1Euqk3O5acEyBICyMKunxxhL94= +go.etcd.io/etcd/api/v3 v3.5.22/go.mod h1:/mQQOEMyP7nAjMKZTJSCtMmlOoNAG5s7IjKZGvMN9Yc= +go.etcd.io/etcd/client/pkg/v3 v3.5.22 h1:dFVbXCohuH0JKo6X+IznK6aohJSWcU+SF8QZF5EsaOQ= +go.etcd.io/etcd/client/pkg/v3 v3.5.22/go.mod h1:cSURbmmXBOqyX839SdTbuO2zYtUjcZvKeQ7DEiYVOmU= +go.etcd.io/etcd/client/v3 v3.5.22 h1:h+y9ocnclCPLPsoBoKwneZy7gy71VjvJ+8z4IPWxTn8= +go.etcd.io/etcd/client/v3 v3.5.22/go.mod h1:p4ROLdCiv8gwQtnozOILXkqIjjSI0MR6Ly5F9qpFg+8= go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -530,6 +502,10 @@ 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= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= @@ -541,8 +517,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -556,8 +532,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -584,10 +560,10 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -599,8 +575,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -611,14 +587,12 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -642,8 +616,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -655,8 +629,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -671,10 +645,10 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -694,22 +668,22 @@ golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= -google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= -google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8= -google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -719,8 +693,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -732,8 +706,6 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -747,41 +719,47 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/cluster-bootstrap v0.32.3 h1:AqIpsUhB6MUeaAsl1WvaUw54AHRd2hfZrESlKChtd8s= -k8s.io/cluster-bootstrap v0.32.3/go.mod h1:CHbBwgOb6liDV6JFUTkx5t85T2xidy0sChBDoyYw344= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/cluster-bootstrap v0.33.3 h1:u2NTxJ5CFSBFXaDxLQoOWMly8eni31psVso+caq6uwI= +k8s.io/cluster-bootstrap v0.33.3/go.mod h1:p970f8u8jf273zyQ5raD8WUu2XyAl0SAWOY82o7i/ds= +k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= 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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cluster-api v1.10.6 h1:0bnLTpT47R8KIvGZ3tTGek0DwMIc8fZi6IxA3Mlqq4g= -sigs.k8s.io/cluster-api v1.10.6/go.mod h1:vymugs3Jm3gxHVMuVqdzgp6BVy/SEqQVyUg/UM7bnT4= -sigs.k8s.io/cluster-api/test v1.10.6 h1:xMRVX91mmvcWihzFpj9a7Zc5Np+zB6zfYoVD6tyww/s= -sigs.k8s.io/cluster-api/test v1.10.6/go.mod h1:t177O+xWeeCqbPXVlsGNn+LKkoyFo9Oh3ar0znVRwX0= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/cluster-api v1.11.11 h1:MvUjPlUYk72ow4HhI9lJlt0C1iY8JsyPtx5YFV8pLJA= +sigs.k8s.io/cluster-api v1.11.11/go.mod h1:wplBEE7AErSt8bN93qrqXeXQ8uEq69RfJBPPYoHWLAE= +sigs.k8s.io/cluster-api/test v1.11.11 h1:KZXBo1j9zfnysVemyX1N61K3S1PqqSvrlhJVQVNDWMk= +sigs.k8s.io/cluster-api/test v1.11.11/go.mod h1:UeTeX/FwCHTBKFylV8640zgjIDBZ3Z+c8jcDB+BfrTM= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= 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/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA= -sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY= +sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index 9428172e..eb8b2530 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ import ( logsv1 "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/cluster-api/controllers/clustercache" "sigs.k8s.io/cluster-api/controllers/remote" "sigs.k8s.io/cluster-api/feature" diff --git a/metadata.yaml b/metadata.yaml index f3b980b3..64d662c5 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -6,6 +6,9 @@ apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3 kind: Metadata releaseSeries: + - major: 1 + minor: 8 + contract: v1beta1 - major: 1 minor: 7 contract: v1beta1 diff --git a/pkg/cloudtower/tower.go b/pkg/cloudtower/tower.go new file mode 100644 index 00000000..f6f5991d --- /dev/null +++ b/pkg/cloudtower/tower.go @@ -0,0 +1,277 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudtower + +import ( + "bytes" + goctx "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + "github.com/go-logr/logr" + httptransport "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + "github.com/pkg/errors" + towerclient "github.com/smartxworks/cloudtower-go-sdk/v2/client" + "github.com/smartxworks/cloudtower-go-sdk/v2/client/user" + "github.com/smartxworks/cloudtower-go-sdk/v2/models" + "golang.org/x/sync/singleflight" + corev1 "k8s.io/api/core/v1" + apitypes "k8s.io/apimachinery/pkg/types" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" + annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" +) + +// constants. +const ( + // CloudTowerServerVersionAnnotation is the annotation identifying the version of cloud tower server configuration. + CloudTowerServerVersionAnnotation = "cape.infrastructure.cluster.x-k8s.io/cloud-server-version" + + CloudTowerServerVersion1_0_0 = "v1.0.0" +) + +var lastGCTime = time.Now() +var gcMinInterval = 10 * time.Minute +var cacheIdleTime = 15 * time.Minute + +// global cache map against cache keys. +// It stores both Tower clients and parsed Tower client configs from immutable Secrets. +var cacheMap sync.Map + +var towerSecretConfigGroup singleflight.Group + +type cacheItem struct { + LastUsedTime time.Time + TowerClient *towerclient.Cloudtower + TowerConfig *infrav1.TowerClientConfig +} + +// NewTowerClient gets a cached client or creates a new one if one does not +// already exist. +func NewTowerClient(ctx goctx.Context, k8sClient client.Client, tower infrav1.Tower) (*towerclient.Cloudtower, error) { + clientConfig, err := GetTowerClientConfig(ctx, k8sClient, tower) + if err != nil { + return nil, err + } + + logger := ctrl.LoggerFrom(ctx).WithName("client").WithValues("server", clientConfig.Server, "username", clientConfig.Username, "source", clientConfig.AuthMode) + + defer func() { + if lastGCTime.Add(gcMinInterval).Before(time.Now()) { + cleanupCache(logger) + } + }() + + clientKey := getTowerClientCacheKey(clientConfig) + if item, ok := loadCacheItem(clientKey); ok && item.TowerClient != nil { + logger.V(3).Info("found active cached tower client") + + return item.TowerClient, nil + } + + client, err := createTowerClient(httptransport.TLSClientOptions{ + InsecureSkipVerify: clientConfig.SkipTLSVerify, + }, towerclient.ClientConfig{ + Host: clientConfig.Server, + BasePath: "/v2/api", + Schemes: []string{"https"}, + }, towerclient.UserConfig{ + Name: clientConfig.Username, + Password: clientConfig.Password, + Source: models.UserSource(clientConfig.AuthMode), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to create tower client") + } + + // Cache the client. + cacheMap.Store(clientKey, &cacheItem{LastUsedTime: time.Now(), TowerClient: client}) + logger.V(3).Info("cached tower client") + + return client, nil +} + +func createTowerClient(tlsOpts httptransport.TLSClientOptions, clientConfig towerclient.ClientConfig, userConfig towerclient.UserConfig) (*towerclient.Cloudtower, error) { + transport := httptransport.New(clientConfig.Host, clientConfig.BasePath, clientConfig.Schemes) + roundTripper, err := httptransport.TLSTransport(tlsOpts) + if err != nil { + return nil, err + } + + // For Arcfra vendor, we need to bypass the whitelist for AOC(CloudTower) + rtWithHeader := NewWithHeaderRoundTripper(roundTripper) + rtWithHeader.Set("x-bypass-whitelist", "true") //nolint:canonicalheader + transport.Transport = rtWithHeader + + client := towerclient.New(transport, strfmt.Default) + params := user.NewLoginParams() + params.WithTimeout(10 * time.Second) + params.RequestBody = &models.LoginInput{ + Username: &userConfig.Name, + Password: &userConfig.Password, + Source: userConfig.Source.Pointer(), + } + resp, err := client.User.Login(params) + if err != nil { + return nil, err + } + transport.DefaultAuthentication = httptransport.APIKeyAuth("Authorization", "header", *resp.Payload.Data.Token) + return client, nil +} + +func cleanupCache(logger logr.Logger) { + cacheMap.Range(func(key interface{}, value interface{}) bool { + item := value.(*cacheItem) + if item.LastUsedTime.Add(cacheIdleTime).Before(time.Now()) { + cacheMap.Delete(key) + logger.V(3).Info(fmt.Sprintf("delete inactive tower cache %s from cacheMap", key)) + } + + return true + }) + + lastGCTime = time.Now() +} + +// ClearClientCache removes all cached Tower clients and client configs. +func ClearClientCache() { + cacheMap = sync.Map{} + lastGCTime = time.Now() +} + +func GetTowerClientConfig(ctx goctx.Context, k8sClient client.Client, tower infrav1.Tower) (*infrav1.TowerClientConfig, error) { + if tower.SecretRef == nil { + return &tower.TowerClientConfig, nil + } + + secretKey := apitypes.NamespacedName{Namespace: tower.SecretRef.Namespace, Name: tower.SecretRef.Name} + cacheKey := getTowerSecretCacheKey(secretKey) + if item, ok := loadCacheItem(cacheKey); ok && item.TowerConfig != nil { + return item.TowerConfig, nil + } + + value, err, _ := towerSecretConfigGroup.Do(cacheKey, func() (interface{}, error) { + if item, ok := loadCacheItem(cacheKey); ok && item.TowerConfig != nil { + return item.TowerConfig, nil + } + + var secret corev1.Secret + if err := k8sClient.Get(ctx, secretKey, &secret); err != nil { + return nil, errors.Wrapf(err, "failed to get tower secret %s", secretKey.String()) + } + + config, err := ParseTowerClientConfigFromSecret(&secret) + if err != nil { + return nil, err + } + + // Cache the config if the server version is annotated. + if annotationsutil.HasAnnotation(&secret, CloudTowerServerVersionAnnotation) { + cacheMap.Store(cacheKey, &cacheItem{LastUsedTime: time.Now(), TowerConfig: config}) + } + + return config, nil + }) + if err != nil { + return nil, err + } + + return value.(*infrav1.TowerClientConfig), nil +} + +func loadCacheItem(cacheKey string) (*cacheItem, bool) { + value, ok := cacheMap.Load(cacheKey) + if !ok { + return nil, false + } + + item := value.(*cacheItem) + item.LastUsedTime = time.Now() + + return item, true +} + +func ParseTowerClientConfigFromSecret(secret *corev1.Secret) (*infrav1.TowerClientConfig, error) { + data, ok := secret.Data["cloudtower.yaml"] + if !ok { + return nil, errors.Errorf("tower secret %s missing cloudtower.yaml", client.ObjectKeyFromObject(secret)) + } + + decoder := utilyaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 1024) + var config infrav1.TowerClientConfig + if err := decoder.Decode(&config); err != nil { + return nil, errors.Wrapf(err, "failed to decode cloudtower.yaml in tower secret %s", client.ObjectKeyFromObject(secret)) + } + + return &config, nil +} + +func getTowerClientCacheKey(tower *infrav1.TowerClientConfig) string { + return "tower-client:" + getClientKey(tower) +} + +func getTowerSecretCacheKey(secretKey apitypes.NamespacedName) string { + return "tower-secret:" + secretKey.String() +} + +func getClientKey(tower *infrav1.TowerClientConfig) string { + encryptedTower := *tower + sum256 := sha256.Sum256([]byte(tower.Password)) + encryptedTower.Password = hex.EncodeToString(sum256[:]) + key, err := json.Marshal(encryptedTower) + if err != nil { + return fmt.Sprintf("%v", encryptedTower) + } + + return string(key) +} + +type withHeaderRoundTripper struct { + http.Header + + rt http.RoundTripper +} + +func NewWithHeaderRoundTripper(rt http.RoundTripper) withHeaderRoundTripper { + if rt == nil { + rt = http.DefaultTransport + } + + return withHeaderRoundTripper{Header: make(http.Header), rt: rt} +} + +func (h withHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if len(h.Header) == 0 { + return h.rt.RoundTrip(req) + } + + req = req.Clone(req.Context()) + for k, v := range h.Header { + req.Header[k] = v + } + + return h.rt.RoundTrip(req) +} diff --git a/pkg/cloudtower/tower_test.go b/pkg/cloudtower/tower_test.go new file mode 100644 index 00000000..de786c1c --- /dev/null +++ b/pkg/cloudtower/tower_test.go @@ -0,0 +1,345 @@ +package cloudtower + +import ( + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/onsi/gomega" + towerclient "github.com/smartxworks/cloudtower-go-sdk/v2/client" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apitypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" +) + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func resetTowerCache(t *testing.T) { + t.Helper() + t.Cleanup(func() { + ClearClientCache() + }) +} + +func TestClearClientCache(t *testing.T) { + t.Run("should clear client cache", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + clientKey := getTowerClientCacheKey(&infrav1.TowerClientConfig{}) + cachedClient := &towerclient.Cloudtower{} + cacheMap.Store(clientKey, &cacheItem{TowerClient: cachedClient}) + + ClearClientCache() + + isEmpty := true + cacheMap.Range(func(key, value any) bool { + isEmpty = true + return false + }) + g.Expect(isEmpty).To(gomega.BeTrue()) + }) +} + +func TestNewTowerClient(t *testing.T) { + t.Run("should get cached session and clear inactive session", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + + lastGCTime = time.Now().Add(-gcMinInterval - time.Second) + tower := infrav1.Tower{TowerClientConfig: infrav1.TowerClientConfig{Server: "127.0.0.1", Username: "tower", Password: "tower"}} + inactiveTowerConfig := tower.TowerClientConfig + inactiveTowerConfig.Username = "inactive" + invalidTower := tower.DeepCopy() + invalidTower.Username = "invalid" + + clientKey := getTowerClientCacheKey(&tower.TowerClientConfig) + cachedClient := &towerclient.Cloudtower{} + cacheMap.Store(clientKey, &cacheItem{TowerClient: cachedClient}) + inactiveClientKey := getTowerClientCacheKey(&inactiveTowerConfig) + cacheMap.Store(inactiveClientKey, &cacheItem{TowerClient: &towerclient.Cloudtower{}, LastUsedTime: time.Now().Add(-cacheIdleTime - time.Second)}) + inactiveSecretKey := getTowerSecretCacheKey(apitypes.NamespacedName{Namespace: "default", Name: "inactive-secret"}) + cacheMap.Store(inactiveSecretKey, &cacheItem{TowerConfig: &infrav1.TowerClientConfig{}, LastUsedTime: time.Now().Add(-cacheIdleTime - time.Second)}) + + client, err := NewTowerClient(t.Context(), nil, tower) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(client).To(gomega.Equal(cachedClient)) + + _, ok := cacheMap.Load(clientKey) + g.Expect(ok).To(gomega.BeTrue()) + _, ok = cacheMap.Load(inactiveClientKey) + g.Expect(ok).To(gomega.BeFalse()) + _, ok = cacheMap.Load(inactiveSecretKey) + g.Expect(ok).To(gomega.BeFalse()) + + client, err = NewTowerClient(t.Context(), nil, *invalidTower) + g.Expect(client).To(gomega.BeNil()) + g.Expect(err).To(gomega.HaveOccurred()) + }) +} + +func TestGetTowerClientConfig(t *testing.T) { + t.Run("returns inline config without requiring secret lookup", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + + tower := infrav1.Tower{TowerClientConfig: infrav1.TowerClientConfig{ + Server: "127.0.0.1", + Username: "tower", + Password: "tower-password", + AuthMode: "LDAP", + SkipTLSVerify: true, + }} + + config, err := GetTowerClientConfig(t.Context(), nil, tower) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config).To(gomega.Equal(&tower.TowerClientConfig)) + }) + + t.Run("reads secret config and reuses cached immutable secret", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + ctx := t.Context() + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cloudtower-server", + Namespace: "sks-system", + Annotations: map[string]string{ + CloudTowerServerVersionAnnotation: CloudTowerServerVersion1_0_0, + }, + }, + Data: map[string][]byte{ + "cloudtower.yaml": []byte("authMode: LOCAL\npassword: K5yt3hcjtUE4Teqe\nserver: 10.255.0.4\nskipTLSVerify: true\nusername: system-service\n"), + }, + } + k8sClient := fake.NewClientBuilder().WithObjects(secret).Build() + + tower := infrav1.Tower{ + TowerClientConfig: infrav1.TowerClientConfig{Server: "127.0.0.1", Username: "ignored", Password: "ignored"}, + SecretRef: &corev1.SecretReference{Name: "cloudtower-server", Namespace: "sks-system"}, + } + config, err := GetTowerClientConfig(ctx, k8sClient, tower) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config).To(gomega.Equal(&infrav1.TowerClientConfig{ + Server: "10.255.0.4", + Username: "system-service", + Password: "K5yt3hcjtUE4Teqe", + AuthMode: "LOCAL", + SkipTLSVerify: true, + })) + + cacheKey := getTowerSecretCacheKey(apitypes.NamespacedName{Namespace: "sks-system", Name: "cloudtower-server"}) + cached, ok := loadCacheItem(cacheKey) + g.Expect(ok).To(gomega.BeTrue()) + g.Expect(cached.TowerConfig).To(gomega.Equal(config)) + + config, err = GetTowerClientConfig(ctx, fake.NewClientBuilder().Build(), tower) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.Server).To(gomega.Equal("10.255.0.4")) + }) + + t.Run("does not cache secret config without server version annotation", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + ctx := t.Context() + secret := newTowerSecret("cloudtower-server", "sks-system", "10.255.0.4", "system-service", "K5yt3hcjtUE4Teqe") + k8sClient := fake.NewClientBuilder().WithObjects(secret).Build() + + tower := infrav1.Tower{SecretRef: &corev1.SecretReference{Name: "cloudtower-server", Namespace: "sks-system"}} + config, err := GetTowerClientConfig(ctx, k8sClient, tower) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(config.Server).To(gomega.Equal("10.255.0.4")) + + cacheKey := getTowerSecretCacheKey(apitypes.NamespacedName{Namespace: "sks-system", Name: "cloudtower-server"}) + _, ok := cacheMap.Load(cacheKey) + g.Expect(ok).To(gomega.BeFalse()) + + _, err = GetTowerClientConfig(ctx, fake.NewClientBuilder().Build(), tower) + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.ContainSubstring("failed to get tower secret sks-system/cloudtower-server")) + }) + + t.Run("uses namespace and name to isolate cached secret configs", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + ctx := t.Context() + secretA := newTowerSecretWithVersion("cloudtower-server", "namespace-a", "10.255.0.4", "user-a", "password-a") + secretB := newTowerSecretWithVersion("cloudtower-server", "namespace-b", "10.255.0.5", "user-b", "password-b") + k8sClient := fake.NewClientBuilder().WithObjects(secretA, secretB).Build() + + configA, err := GetTowerClientConfig(ctx, k8sClient, infrav1.Tower{SecretRef: &corev1.SecretReference{Name: "cloudtower-server", Namespace: "namespace-a"}}) + g.Expect(err).ToNot(gomega.HaveOccurred()) + configB, err := GetTowerClientConfig(ctx, k8sClient, infrav1.Tower{SecretRef: &corev1.SecretReference{Name: "cloudtower-server", Namespace: "namespace-b"}}) + g.Expect(err).ToNot(gomega.HaveOccurred()) + + g.Expect(configA.Server).To(gomega.Equal("10.255.0.4")) + g.Expect(configA.Username).To(gomega.Equal("user-a")) + g.Expect(configB.Server).To(gomega.Equal("10.255.0.5")) + g.Expect(configB.Username).To(gomega.Equal("user-b")) + }) + + t.Run("returns contextual errors for missing secret", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + resetTowerCache(t) + + _, err := GetTowerClientConfig(t.Context(), fake.NewClientBuilder().Build(), infrav1.Tower{ + SecretRef: &corev1.SecretReference{Name: "missing", Namespace: "sks-system"}, + }) + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.ContainSubstring("failed to get tower secret sks-system/missing")) + }) +} + +func TestParseTowerClientConfigFromSecret(t *testing.T) { + t.Run("parses yaml and json cloudtower config", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + yamlConfig, err := ParseTowerClientConfigFromSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "yaml-secret", Namespace: "sks-system"}, + Data: map[string][]byte{ + "cloudtower.yaml": []byte("authMode: LOCAL\npassword: yaml-password\nserver: 10.255.0.4\nskipTLSVerify: true\nusername: yaml-user\n"), + }, + }) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(yamlConfig).To(gomega.Equal(&infrav1.TowerClientConfig{Server: "10.255.0.4", Username: "yaml-user", Password: "yaml-password", AuthMode: "LOCAL", SkipTLSVerify: true})) + + jsonConfig, err := ParseTowerClientConfigFromSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "json-secret", Namespace: "sks-system"}, + Data: map[string][]byte{ + "cloudtower.yaml": []byte(`{"authMode":"LDAP","password":"json-password","server":"10.255.0.5","skipTLSVerify":false,"username":"json-user"}`), + }, + }) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(jsonConfig).To(gomega.Equal(&infrav1.TowerClientConfig{Server: "10.255.0.5", Username: "json-user", Password: "json-password", AuthMode: "LDAP"})) + }) + + t.Run("rejects missing cloudtower yaml key", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + _, err := ParseTowerClientConfigFromSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-secret", Namespace: "sks-system"}, + Data: map[string][]byte{"other.yaml": []byte("server: 10.255.0.4\n")}, + }) + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.ContainSubstring("tower secret sks-system/invalid-secret missing cloudtower.yaml")) + }) + + t.Run("rejects malformed cloudtower yaml", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + _, err := ParseTowerClientConfigFromSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "malformed-secret", Namespace: "sks-system"}, + Data: map[string][]byte{"cloudtower.yaml": []byte("server: [unterminated\n")}, + }) + g.Expect(err).To(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.ContainSubstring("failed to decode cloudtower.yaml in tower secret sks-system/malformed-secret")) + }) +} + +func TestTowerCacheKeys(t *testing.T) { + t.Run("client cache key hashes password without exposing plaintext", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + config := &infrav1.TowerClientConfig{Server: "10.255.0.4", Username: "system-service", Password: "super-secret", AuthMode: "LOCAL", SkipTLSVerify: true} + key := getTowerClientCacheKey(config) + + g.Expect(key).To(gomega.HavePrefix("tower-client:")) + g.Expect(key).ToNot(gomega.ContainSubstring("super-secret")) + g.Expect(key).To(gomega.ContainSubstring("system-service")) + g.Expect(config.Password).To(gomega.Equal("super-secret")) + }) + + t.Run("client cache key changes when password changes", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + base := &infrav1.TowerClientConfig{Server: "10.255.0.4", Username: "system-service", Password: "password-a", AuthMode: "LOCAL"} + changedPassword := *base + changedPassword.Password = "password-b" + + g.Expect(getTowerClientCacheKey(base)).ToNot(gomega.Equal(getTowerClientCacheKey(&changedPassword))) + }) + + t.Run("secret cache key includes namespace and name", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + g.Expect(getTowerSecretCacheKey(apitypes.NamespacedName{Namespace: "sks-system", Name: "cloudtower-server"})).To(gomega.Equal("tower-secret:sks-system/cloudtower-server")) + }) +} + +func TestWithHeaderRoundTripper(t *testing.T) { + t.Run("uses default transport when nil delegate is provided", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + rt := NewWithHeaderRoundTripper(nil) + + g.Expect(rt.rt).ToNot(gomega.BeNil()) + }) + + t.Run("does not clone or mutate headers when no configured headers exist", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + called := false + req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) + g.Expect(err).ToNot(gomega.HaveOccurred()) + req.Header.Set("Existing", "value") + + rt := NewWithHeaderRoundTripper(roundTripFunc(func(got *http.Request) (*http.Response, error) { + called = true + g.Expect(got).To(gomega.BeIdenticalTo(req)) + g.Expect(got.Header.Get("Existing")).To(gomega.Equal("value")) + return &http.Response{StatusCode: http.StatusNoContent, Body: io.NopCloser(strings.NewReader("")), Header: make(http.Header)}, nil + })) + + resp, err := rt.RoundTrip(req) + g.Expect(err).ToNot(gomega.HaveOccurred()) + defer resp.Body.Close() + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusNoContent)) + g.Expect(called).To(gomega.BeTrue()) + }) + + t.Run("adds configured headers to a cloned request", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + req, err := http.NewRequest(http.MethodGet, "https://example.com", nil) + g.Expect(err).ToNot(gomega.HaveOccurred()) + req.Header.Set("Existing", "value") + + rt := NewWithHeaderRoundTripper(roundTripFunc(func(got *http.Request) (*http.Response, error) { + g.Expect(got).ToNot(gomega.BeIdenticalTo(req)) + g.Expect(got.Header.Get("Existing")).To(gomega.Equal("value")) + g.Expect(got.Header.Values("X-Test")).To(gomega.Equal([]string{"a", "b"})) + return &http.Response{StatusCode: http.StatusAccepted, Body: io.NopCloser(strings.NewReader("")), Header: make(http.Header)}, nil + })) + rt.Set("X-Test", "a") + rt.Add("X-Test", "b") + + resp, err := rt.RoundTrip(req) + g.Expect(err).ToNot(gomega.HaveOccurred()) + defer resp.Body.Close() + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusAccepted)) + g.Expect(req.Header.Values("X-Test")).To(gomega.BeEmpty()) + }) +} + +func newTowerSecret(name, namespace, server, username, password string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, + Data: map[string][]byte{ + "cloudtower.yaml": []byte("authMode: LOCAL\npassword: " + password + "\nserver: " + server + "\nskipTLSVerify: true\nusername: " + username + "\n"), + }, + } +} + +func newTowerSecretWithVersion(name, namespace, server, username, password string) *corev1.Secret { + secret := newTowerSecret(name, namespace, server, username, password) + secret.Annotations = map[string]string{ + CloudTowerServerVersionAnnotation: CloudTowerServerVersion1_0_0, + } + return secret +} diff --git a/pkg/config/vm.go b/pkg/config/vm.go index eb8c42ee..8a9f0cec 100644 --- a/pkg/config/vm.go +++ b/pkg/config/vm.go @@ -18,7 +18,7 @@ package config const ( // VMDescription is the default description in a VM. - VMDescription = "Automatically created Kubernetes node by server %s." + VMDescription = "Automatically created Kubernetes node." ) // MaxConcurrentVMCreations is the maximum number of concurrent virtual machine creations. diff --git a/pkg/context/cluster_context.go b/pkg/context/cluster_context.go index e9cfc493..c22daea3 100644 --- a/pkg/context/cluster_context.go +++ b/pkg/context/cluster_context.go @@ -21,8 +21,8 @@ import ( "fmt" "github.com/go-logr/logr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/patch" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" diff --git a/pkg/context/machine_context.go b/pkg/context/machine_context.go index 76081add..2552db8d 100644 --- a/pkg/context/machine_context.go +++ b/pkg/context/machine_context.go @@ -20,9 +20,10 @@ import ( goctx "context" "fmt" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "github.com/smartxworks/cloudtower-go-sdk/v2/models" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" @@ -94,6 +95,28 @@ func (c *MachineContext) GetElfClusterID() string { return "" } +// GetStorageConfig returns the cluster-level storage configuration for the machine. +func (c *MachineContext) GetStorageConfig() *models.StorageConfig { + if c == nil || c.ElfCluster == nil { + return nil + } + + sc := c.ElfCluster.Spec.StorageCluster + if sc.DatastoreID == "" && sc.StorageClusterID == "" { + return nil + } + + storageConfig := &models.StorageConfig{} + if sc.DatastoreID != "" { + storageConfig.DatastoreID = service.TowerString(sc.DatastoreID) + } + if sc.StorageClusterID != "" { + storageConfig.StorageClusterID = service.TowerString(sc.StorageClusterID) + } + + return storageConfig +} + // GenerateHostname generates a hostname for the machine. // If a HostNamePrefix is specified in the failure domain or machine spec, the hostname will be generated as -. // Otherwise, the machine name will be used as the hostname. diff --git a/pkg/context/machine_context_test.go b/pkg/context/machine_context_test.go index 52003ce7..a2eaab39 100644 --- a/pkg/context/machine_context_test.go +++ b/pkg/context/machine_context_test.go @@ -20,10 +20,12 @@ import ( "testing" . "github.com/onsi/gomega" + "github.com/smartxworks/cloudtower-go-sdk/v2/models" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" + "github.com/smartxworks/cluster-api-provider-elf/pkg/service" ) func TestMachineContextString(t *testing.T) { @@ -237,6 +239,44 @@ func TestGetElfClusterID(t *testing.T) { } } +func TestGetStorageConfig(t *testing.T) { + tests := []struct { + name string + elfCluster *infrav1.ElfCluster + expected *models.StorageConfig + }{ + { + name: "When ElfCluster is nil should return nil", + elfCluster: nil, + expected: nil, + }, + { + name: "When storage config is not set should return nil", + elfCluster: &infrav1.ElfCluster{}, + expected: nil, + }, + { + name: "When one storage config is set should return it", + elfCluster: &infrav1.ElfCluster{ + Spec: infrav1.ElfClusterSpec{ + StorageCluster: infrav1.StorageConfig{StorageClusterID: "storage-cluster"}, + }, + }, + expected: &models.StorageConfig{StorageClusterID: service.TowerString("storage-cluster")}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + ctx := &MachineContext{ + ElfCluster: tc.elfCluster, + } + g.Expect(ctx.GetStorageConfig()).To(Equal(tc.expected)) + }) + } +} + // strPtr is a helper to get a pointer to a string literal. func strPtr(s string) *string { return &s diff --git a/pkg/context/machine_template_context.go b/pkg/context/machine_template_context.go index 98daf60b..ecda8197 100644 --- a/pkg/context/machine_template_context.go +++ b/pkg/context/machine_template_context.go @@ -19,7 +19,7 @@ package context import ( "fmt" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/service" diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 60577d0a..f8f5564e 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -23,9 +23,10 @@ import ( agentv1 "github.com/smartxworks/host-config-agent-api/api/v1alpha1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" cgscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + bootstrapv1 "sigs.k8s.io/cluster-api/api/bootstrap/kubeadm/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" ctrl "sigs.k8s.io/controller-runtime" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -48,6 +49,7 @@ func New(ctx goctx.Context, opts Options) (Manager, error) { utilruntime.Must(cgscheme.AddToScheme(opts.Scheme)) utilruntime.Must(clusterv1.AddToScheme(opts.Scheme)) + utilruntime.Must(clusterv2.AddToScheme(opts.Scheme)) utilruntime.Must(infrav1.AddToScheme(opts.Scheme)) utilruntime.Must(bootstrapv1.AddToScheme(opts.Scheme)) utilruntime.Must(controlplanev1.AddToScheme(opts.Scheme)) diff --git a/pkg/resources/placement_group.go b/pkg/resources/placement_group.go index 6d06e893..c9a65c1f 100644 --- a/pkg/resources/placement_group.go +++ b/pkg/resources/placement_group.go @@ -21,7 +21,7 @@ import ( "fmt" "github.com/smartxworks/cloudtower-go-sdk/v2/models" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" diff --git a/pkg/service/vm.go b/pkg/service/vm.go index 7c3e27d6..0b77f536 100644 --- a/pkg/service/vm.go +++ b/pkg/service/vm.go @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" "github.com/pkg/errors" + towerclient "github.com/smartxworks/cloudtower-go-sdk/v2/client" clientcluster "github.com/smartxworks/cloudtower-go-sdk/v2/client/cluster" clientvmtemplate "github.com/smartxworks/cloudtower-go-sdk/v2/client/content_library_vm_template" clientgpu "github.com/smartxworks/cloudtower-go-sdk/v2/client/gpu_device" @@ -38,10 +39,11 @@ import ( clientzone "github.com/smartxworks/cloudtower-go-sdk/v2/client/zone" "github.com/smartxworks/cloudtower-go-sdk/v2/models" "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" + "github.com/smartxworks/cluster-api-provider-elf/pkg/cloudtower" "github.com/smartxworks/cluster-api-provider-elf/pkg/config" - "github.com/smartxworks/cluster-api-provider-elf/pkg/session" annotationsutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/annotations" ) @@ -54,11 +56,12 @@ var ( ) type CloneVMInfo struct { - Cluster string `json:"cluster,omitempty"` - Host string `json:"host,omitempty"` - CloudInit string `json:"cloudInit,omitempty"` - GPUDevices []*GPUDeviceInfo `json:"gpuDevices,omitempty"` - HostName string `json:"hostName,omitempty"` + Cluster string `json:"cluster,omitempty"` + StorageConfig *models.StorageConfig `json:"storageConfig,omitempty"` + Host string `json:"host,omitempty"` + CloudInit string `json:"cloudInit,omitempty"` + GPUDevices []*GPUDeviceInfo `json:"gpuDevices,omitempty"` + HostName string `json:"hostName,omitempty"` } type VMService interface { @@ -102,20 +105,20 @@ type VMService interface { GetVMGPUAllocationInfo(id string) (*models.VMGpuInfo, error) } -type NewVMServiceFunc func(ctx goctx.Context, auth infrav1.Tower, logger logr.Logger) (VMService, error) +type NewVMServiceFunc func(ctx goctx.Context, k8sClient client.Client, tower infrav1.Tower, logger logr.Logger) (VMService, error) -func NewVMService(ctx goctx.Context, auth infrav1.Tower, logger logr.Logger) (VMService, error) { - authSession, err := session.GetOrCreate(ctx, auth) +func NewVMService(ctx goctx.Context, k8sClient client.Client, tower infrav1.Tower, logger logr.Logger) (VMService, error) { + towerClient, err := cloudtower.NewTowerClient(ctx, k8sClient, tower) if err != nil { return nil, err } - return &TowerVMService{authSession, logger}, nil + return &TowerVMService{towerClient, logger}, nil } type TowerVMService struct { - Session *session.TowerSession `json:"session"` - Logger logr.Logger `json:"logger"` + Client *towerclient.Cloudtower `json:"towerClient"` + Logger logr.Logger `json:"logger"` } func (svr *TowerVMService) UpdateVM(vm *models.VM, elfMachine *infrav1.ElfMachine) (*models.WithTaskVM, error) { @@ -135,7 +138,7 @@ func (svr *TowerVMService) UpdateVM(vm *models.VM, elfMachine *infrav1.ElfMachin Where: &models.VMWhereInput{ID: TowerString(*vm.ID)}, } - updateVMResp, err := svr.Session.VM.UpdateVM(updateVMParams) + updateVMResp, err := svr.Client.VM.UpdateVM(updateVMParams) if err != nil { return nil, err } @@ -150,7 +153,7 @@ func (svr *TowerVMService) GetVMDisks(vmDiskIDs []string) ([]*models.VMDisk, err OrderBy: models.NewVMDiskOrderByInput(models.VMDiskOrderByInputBootASC), } - getVMDisksResp, err := svr.Session.VMDisk.GetVMDisks(getVMDisksParams) + getVMDisksResp, err := svr.Client.VMDisk.GetVMDisks(getVMDisksParams) if err != nil { return nil, err } @@ -164,7 +167,7 @@ func (svr *TowerVMService) GetVMVolume(volumeID string) (*models.VMVolume, error Where: &models.VMVolumeWhereInput{ID: TowerString(volumeID)}, } - getVMVolumesResp, err := svr.Session.VMVolume.GetVMVolumes(getVMVolumesParams) + getVMVolumesResp, err := svr.Client.VMVolume.GetVMVolumes(getVMVolumesParams) if err != nil { return nil, err } @@ -185,7 +188,7 @@ func (svr *TowerVMService) ResizeVMVolume(vmVolumeID string, size int64) (*model Where: &models.VMVolumeWhereInput{ID: TowerString(vmVolumeID)}, } - updateVMVolumeResp, err := svr.Session.VMVolume.UpdateVMVolume(updateVMVolumeParams) + updateVMVolumeResp, err := svr.Client.VMVolume.UpdateVMVolume(updateVMVolumeParams) if err != nil { return nil, err } @@ -213,7 +216,7 @@ func (svr *TowerVMService) Clone( createVMFromContentLibraryTemplateParams := clientvm.NewCreateVMFromContentLibraryTemplateParams() createVMFromContentLibraryTemplateParams.RequestBody = []*models.VMCreateVMFromContentLibraryTemplateParams{createVMFromTemplateParams} - createVMFromTemplateResp, err := svr.Session.VM.CreateVMFromContentLibraryTemplate(createVMFromContentLibraryTemplateParams) + createVMFromTemplateResp, err := svr.Client.VM.CreateVMFromContentLibraryTemplate(createVMFromContentLibraryTemplateParams) if err != nil { return nil, err } @@ -385,30 +388,32 @@ func (svr *TowerVMService) createVMFromTemplateParams( Size: disk.Size, ElfStoragePolicy: models.NewVMVolumeElfStoragePolicyType(models.VMVolumeElfStoragePolicyTypeREPLICA3THINPROVISION), }, + StorageConfig: vmInfo.StorageConfig, }) } } return &models.VMCreateVMFromContentLibraryTemplateParams{ - ClusterID: cluster.ID, - HostID: TowerString(hostID), - Name: TowerString(elfMachine.Name), - Description: TowerString(fmt.Sprintf(config.VMDescription, elfCluster.Spec.Tower.Server)), - Owner: owner, - Vcpu: vCPU, - CPUCores: cpuSocketCores, - CPUSockets: cpuSockets, - Memory: TowerMemory(elfMachine.Spec.MemoryMiB), - GpuDevices: gpuDevices, - Status: models.NewVMStatus(models.VMStatusSTOPPED), - Ha: ha, - HaPriority: haPriority, - IsFullCopy: TowerBool(isFullCopy), - TemplateID: template.ID, - GuestOsType: models.NewVMGuestsOperationSystem(models.VMGuestsOperationSystem(elfMachine.Spec.OSType)), - VMNics: nics, - DiskOperate: diskOperate, - CloudInit: cloudInit, + ClusterID: cluster.ID, + HostID: TowerString(hostID), + Name: TowerString(elfMachine.Name), + Description: TowerString(config.VMDescription), + Owner: owner, + Vcpu: vCPU, + CPUCores: cpuSocketCores, + CPUSockets: cpuSockets, + Memory: TowerMemory(elfMachine.Spec.MemoryMiB), + GpuDevices: gpuDevices, + Status: models.NewVMStatus(models.VMStatusSTOPPED), + Ha: ha, + HaPriority: haPriority, + IsFullCopy: TowerBool(isFullCopy), + TemplateID: template.ID, + GuestOsType: models.NewVMGuestsOperationSystem(models.VMGuestsOperationSystem(elfMachine.Spec.OSType)), + VMNics: nics, + DiskOperate: diskOperate, + StorageConfig: vmInfo.StorageConfig, + CloudInit: cloudInit, }, nil } @@ -423,7 +428,7 @@ func (svr *TowerVMService) Migrate(vmID, hostID string) (*models.WithTaskVM, err }, } - migrateVMResp, err := svr.Session.VM.MigrateVM(migrateVMParams) + migrateVMResp, err := svr.Client.VM.MigrateVM(migrateVMParams) if err != nil { return nil, err } @@ -440,7 +445,7 @@ func (svr *TowerVMService) Delete(id string) (*models.Task, error) { }, } - deleteVMResp, err := svr.Session.VM.DeleteVM(deleteVMParams) + deleteVMResp, err := svr.Client.VM.DeleteVM(deleteVMParams) if err != nil { return nil, err } @@ -461,7 +466,7 @@ func (svr *TowerVMService) PowerOff(id string) (*models.Task, error) { }, } - poweroffVMResp, err := svr.Session.VM.PoweroffVM(poweroffVMParams) + poweroffVMResp, err := svr.Client.VM.PoweroffVM(poweroffVMParams) if err != nil { return nil, err } @@ -489,7 +494,7 @@ func (svr *TowerVMService) PowerOn(id string, hostID string) (*models.Task, erro startVMParams.RequestBody.Data = &models.VMStartParamsData{HostID: TowerString(hostID)} } - startVMResp, err := svr.Session.VM.StartVM(startVMParams) + startVMResp, err := svr.Client.VM.StartVM(startVMParams) if err != nil { return nil, err } @@ -510,7 +515,7 @@ func (svr *TowerVMService) ShutDown(id string) (*models.Task, error) { }, } - shutDownVMResp, err := svr.Session.VM.ShutDownVM(shutDownVMParams) + shutDownVMResp, err := svr.Client.VM.ShutDownVM(shutDownVMParams) if err != nil { return nil, err } @@ -531,7 +536,7 @@ func (svr *TowerVMService) RemoveGPUDevices(id string, gpus []*models.VMGpuOpera }, } - temoveVMGPUDeviceResp, err := svr.Session.VM.RemoveVMGpuDevice(removeVMGpuDeviceParams) + temoveVMGPUDeviceResp, err := svr.Client.VM.RemoveVMGpuDevice(removeVMGpuDeviceParams) if err != nil { return nil, err } @@ -560,7 +565,7 @@ func (svr *TowerVMService) AddGPUDevices(id string, gpuDeviceInfos []*GPUDeviceI }, } - addVMGpuDeviceResp, err := svr.Session.VM.AddVMGpuDevice(addVMGpuDeviceParams) + addVMGpuDeviceResp, err := svr.Client.VM.AddVMGpuDevice(addVMGpuDeviceParams) if err != nil { return nil, err } @@ -581,7 +586,7 @@ func (svr *TowerVMService) Get(id string) (*models.VM, error) { }, } - getVmsResp, err := svr.Session.VM.GetVms(getVmsParams) + getVmsResp, err := svr.Client.VM.GetVms(getVmsParams) if err != nil { return nil, err } @@ -602,7 +607,7 @@ func (svr *TowerVMService) GetByName(name string) (*models.VM, error) { }, } - getVmsResp, err := svr.Session.VM.GetVms(getVmsParams) + getVmsResp, err := svr.Client.VM.GetVms(getVmsParams) if err != nil { return nil, err } @@ -627,7 +632,7 @@ func (svr *TowerVMService) FindByIDs(ids []string) ([]*models.VM, error) { }, } - getVmsResp, err := svr.Session.VM.GetVms(getVmsParams) + getVmsResp, err := svr.Client.VM.GetVms(getVmsParams) if err != nil { return nil, err } @@ -648,7 +653,7 @@ func (svr *TowerVMService) FindVMsByName(name string) ([]*models.VM, error) { }, } - getVmsResp, err := svr.Session.VM.GetVms(getVmsParams) + getVmsResp, err := svr.Client.VM.GetVms(getVmsParams) if err != nil { return nil, err } @@ -668,7 +673,7 @@ func (svr *TowerVMService) GetVMNics(vmID string) ([]*models.VMNic, error) { OrderBy: models.NewVMNicOrderByInput(models.VMNicOrderByInputOrderASC), } - getVMNicsResp, err := svr.Session.VMNic.GetVMNics(getVMNicsParams) + getVMNicsResp, err := svr.Client.VMNic.GetVMNics(getVMNicsParams) if err != nil { return nil, err } @@ -687,7 +692,7 @@ func (svr *TowerVMService) AddVMNics(vmID string, nics []*models.VMNicParams) (* }, } - addVMNicResp, err := svr.Session.VM.AddVMNic(addVMNicParams) + addVMNicResp, err := svr.Client.VM.AddVMNic(addVMNicParams) if err != nil { return nil, err } @@ -708,7 +713,7 @@ func (svr *TowerVMService) GetCluster(id string) (*models.Cluster, error) { }, } - getClustersResp, err := svr.Session.Cluster.GetClusters(getClustersParams) + getClustersResp, err := svr.Client.Cluster.GetClusters(getClustersParams) if err != nil { return nil, err } @@ -730,7 +735,7 @@ func (svr *TowerVMService) GetClusterZones(clusterID string) ([]*models.Zone, er }, } - getZonesResp, err := svr.Session.Zone.GetZones(getZonesParams) + getZonesResp, err := svr.Client.Zone.GetZones(getZonesParams) if err != nil { return nil, err } @@ -746,7 +751,7 @@ func (svr *TowerVMService) GetHost(id string) (*models.Host, error) { }, } - getHostsResp, err := svr.Session.Host.GetHosts(getHostsParams) + getHostsResp, err := svr.Client.Host.GetHosts(getHostsParams) if err != nil { return nil, err } @@ -768,7 +773,7 @@ func (svr *TowerVMService) GetHostsByCluster(clusterID string) (Hosts, error) { }, } - getHostsResp, err := svr.Session.Host.GetHosts(getHostsParams) + getHostsResp, err := svr.Client.Host.GetHosts(getHostsParams) if err != nil { return nil, err } @@ -789,7 +794,7 @@ func (svr *TowerVMService) GetVlan(id string) (*models.Vlan, error) { }, } - getVlansResp, err := svr.Session.Vlan.GetVlans(getVlansParams) + getVlansResp, err := svr.Client.Vlan.GetVlans(getVlansParams) if err != nil { return nil, err } @@ -820,7 +825,7 @@ func (svr *TowerVMService) GetVMTemplate(template string) (*models.ContentLibrar }, } - getVMTemplatesResp, err := svr.Session.ContentLibraryVMTemplate.GetContentLibraryVMTemplates(getVMTemplatesParams) + getVMTemplatesResp, err := svr.Client.ContentLibraryVMTemplate.GetContentLibraryVMTemplates(getVMTemplatesParams) if err != nil { return nil, err } @@ -849,7 +854,7 @@ func (svr *TowerVMService) GetTask(id string) (*models.Task, error) { }, } - getTasksResp, err := svr.Session.Task.GetTasks(getTasksParams) + getTasksResp, err := svr.Client.Task.GetTasks(getTasksParams) if err != nil { return nil, err } @@ -894,7 +899,7 @@ func (svr *TowerVMService) UpsertLabel(key, value string) (*models.Label, error) Value: TowerString(value), }, } - getLabelResp, err := svr.Session.Label.GetLabels(getLabelParams) + getLabelResp, err := svr.Client.Label.GetLabels(getLabelParams) if err != nil { return nil, err } @@ -906,7 +911,7 @@ func (svr *TowerVMService) UpsertLabel(key, value string) (*models.Label, error) createLabelParams.RequestBody = []*models.LabelCreationParams{ {Key: &key, Value: &value}, } - createLabelResp, err := svr.Session.Label.CreateLabel(createLabelParams) + createLabelResp, err := svr.Client.Label.CreateLabel(createLabelParams) if err != nil { return nil, err } @@ -936,7 +941,7 @@ func (svr *TowerVMService) DeleteLabel(key, value string, strict bool) (string, ) } - deleteLabelResp, err := svr.Session.Label.DeleteLabel(deleteLabelParams) + deleteLabelResp, err := svr.Client.Label.DeleteLabel(deleteLabelParams) if err != nil { return "", err } @@ -960,7 +965,7 @@ func (svr *TowerVMService) CleanUnusedLabels(keys []string) ([]string, error) { }, } - deleteLabelResp, err := svr.Session.Label.DeleteLabel(deleteLabelParams) + deleteLabelResp, err := svr.Client.Label.DeleteLabel(deleteLabelParams) if err != nil { return nil, err } @@ -986,7 +991,7 @@ func (svr *TowerVMService) AddLabelsToVM(vmID string, labelIds []string) (*model }, }, } - addLabelsResp, err := svr.Session.Label.AddLabelsToResources(addLabelsParams) + addLabelsResp, err := svr.Client.Label.AddLabelsToResources(addLabelsParams) if err != nil { return nil, err } @@ -1009,7 +1014,7 @@ func (svr *TowerVMService) CreateVMPlacementGroup(name, clusterID string, vmPoli VMVMPolicyEnabled: TowerBool(true), VMVMPolicy: &vmPolicy, }} - createVMPlacementGroupResp, err := svr.Session.VMPlacementGroup.CreateVMPlacementGroup(createVMPlacementGroupParams) + createVMPlacementGroupResp, err := svr.Client.VMPlacementGroup.CreateVMPlacementGroup(createVMPlacementGroupParams) if err != nil { return nil, err } @@ -1026,7 +1031,7 @@ func (svr *TowerVMService) GetVMPlacementGroup(name string) (*models.VMPlacement }, } - getVMPlacementGroupsResp, err := svr.Session.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) + getVMPlacementGroupsResp, err := svr.Client.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) if err != nil { return nil, err } @@ -1057,7 +1062,7 @@ func (svr *TowerVMService) AddVMsToPlacementGroup(placementGroup *models.VMPlace }, } - updateVMPlacementGroupResp, err := svr.Session.VMPlacementGroup.UpdateVMPlacementGroup(updateVMPlacementGroupParams) + updateVMPlacementGroupResp, err := svr.Client.VMPlacementGroup.UpdateVMPlacementGroup(updateVMPlacementGroupParams) if err != nil { return nil, err } @@ -1078,7 +1083,7 @@ func (svr *TowerVMService) DeleteVMPlacementGroupByID(ctx goctx.Context, id stri }, } - getVMPlacementGroupsResp, err := svr.Session.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) + getVMPlacementGroupsResp, err := svr.Client.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) if err != nil { return false, err } @@ -1096,7 +1101,7 @@ func (svr *TowerVMService) DeleteVMPlacementGroupByID(ctx goctx.Context, id stri }, } - if _, err := svr.Session.VMPlacementGroup.DeleteVMPlacementGroup(deleteVMPlacementGroupParams); err != nil { + if _, err := svr.Client.VMPlacementGroup.DeleteVMPlacementGroup(deleteVMPlacementGroupParams); err != nil { return false, err } @@ -1120,7 +1125,7 @@ func (svr *TowerVMService) DeleteVMPlacementGroupsByNamePrefix(ctx goctx.Context }, } - getVMPlacementGroupsResp, err := svr.Session.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) + getVMPlacementGroupsResp, err := svr.Client.VMPlacementGroup.GetVMPlacementGroups(getVMPlacementGroupsParams) if err != nil { return nil, err } else if len(getVMPlacementGroupsResp.Payload) == 0 { @@ -1135,7 +1140,7 @@ func (svr *TowerVMService) DeleteVMPlacementGroupsByNamePrefix(ctx goctx.Context }, } - deleteVMPlacementGroupResp, err := svr.Session.VMPlacementGroup.DeleteVMPlacementGroup(deleteVMPlacementGroupParams) + deleteVMPlacementGroupResp, err := svr.Client.VMPlacementGroup.DeleteVMPlacementGroup(deleteVMPlacementGroupParams) if err != nil { return nil, err } @@ -1180,7 +1185,7 @@ func (svr *TowerVMService) GetGPUDevicesAllocationInfoByIDs(gpuIDs []string) (GP }, } - getDetailVMInfoByGpuDevicesResp, err := svr.Session.GpuDevice.GetDetailVMInfoByGpuDevices(getDetailVMInfoByGpuDevicesParams) + getDetailVMInfoByGpuDevicesResp, err := svr.Client.GpuDevice.GetDetailVMInfoByGpuDevices(getDetailVMInfoByGpuDevicesParams) if err != nil { return nil, err } @@ -1209,7 +1214,7 @@ func (svr *TowerVMService) GetGPUDevicesAllocationInfoByHostIDs(hostIDs []string getDetailVMInfoByGpuDevicesParams.RequestBody.Where.AvailableVgpusNumGt = TowerInt32(0) } - getDetailVMInfoByGpuDevicesResp, err := svr.Session.GpuDevice.GetDetailVMInfoByGpuDevices(getDetailVMInfoByGpuDevicesParams) + getDetailVMInfoByGpuDevicesResp, err := svr.Client.GpuDevice.GetDetailVMInfoByGpuDevices(getDetailVMInfoByGpuDevicesParams) if err != nil { return nil, err } @@ -1226,7 +1231,7 @@ func (svr *TowerVMService) GetVMGPUAllocationInfo(id string) (*models.VMGpuInfo, }, } - getVMGpuDeviceInfoResp, err := svr.Session.VM.GetVMGpuDeviceInfo(getVMGpuDeviceInfoParams) + getVMGpuDeviceInfoResp, err := svr.Client.VM.GetVMGpuDeviceInfo(getVMGpuDeviceInfoParams) if err != nil { return nil, err } diff --git a/pkg/service/vm_test.go b/pkg/service/vm_test.go new file mode 100644 index 00000000..37bc712d --- /dev/null +++ b/pkg/service/vm_test.go @@ -0,0 +1,145 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "encoding/json" + "testing" + + "github.com/onsi/gomega" + "github.com/smartxworks/cloudtower-go-sdk/v2/models" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" +) + +func TestCreateVMFromTemplateParams(t *testing.T) { + t.Run("when storage cluster is set should use top-level storage config", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + params := newTestCreateVMFromTemplateParams(t, infrav1.ElfClusterTypeStandard, &models.StorageConfig{StorageClusterID: TowerString("storage-cluster")}) + + g.Expect(params.StorageConfig).NotTo(gomega.BeNil()) + g.Expect(*params.StorageConfig.StorageClusterID).To(gomega.Equal("storage-cluster")) + + serialized, err := json.Marshal(params) + g.Expect(err).NotTo(gomega.HaveOccurred()) + var payload struct { + StorageConfig *models.StorageConfig `json:"storage_config,omitempty"` + } + g.Expect(json.Unmarshal(serialized, &payload)).To(gomega.Succeed()) + g.Expect(payload.StorageConfig).NotTo(gomega.BeNil()) + g.Expect(*payload.StorageConfig.StorageClusterID).To(gomega.Equal("storage-cluster")) + }) + + t.Run("when datastore is set should use top-level storage config", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + params := newTestCreateVMFromTemplateParams(t, infrav1.ElfClusterTypeStandard, &models.StorageConfig{DatastoreID: TowerString("datastore")}) + + g.Expect(params.StorageConfig).NotTo(gomega.BeNil()) + g.Expect(*params.StorageConfig.DatastoreID).To(gomega.Equal("datastore")) + g.Expect(params.StorageConfig.StorageClusterID).To(gomega.BeNil()) + }) + + t.Run("when storage cluster is unset should omit storage config", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + params := newTestCreateVMFromTemplateParams(t, infrav1.ElfClusterTypeStandard, nil) + + g.Expect(params.StorageConfig).To(gomega.BeNil()) + + serialized, err := json.Marshal(params) + g.Expect(err).NotTo(gomega.HaveOccurred()) + var payload map[string]json.RawMessage + g.Expect(json.Unmarshal(serialized, &payload)).To(gomega.Succeed()) + g.Expect(payload).NotTo(gomega.HaveKey("storage_config")) + }) + + t.Run("when stretched and storage cluster is set should use disk storage config", func(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + params := newTestCreateVMFromTemplateParams(t, infrav1.ElfClusterTypeStretched, &models.StorageConfig{StorageClusterID: TowerString("storage-cluster")}) + + g.Expect(params.DiskOperate).NotTo(gomega.BeNil()) + g.Expect(params.DiskOperate.NewDisks.MountNewCreateDisks).To(gomega.HaveLen(1)) + g.Expect(params.DiskOperate.NewDisks.MountNewCreateDisks[0].StorageConfig).NotTo(gomega.BeNil()) + g.Expect(*params.DiskOperate.NewDisks.MountNewCreateDisks[0].StorageConfig.StorageClusterID).To(gomega.Equal("storage-cluster")) + + serialized, err := json.Marshal(params) + g.Expect(err).NotTo(gomega.HaveOccurred()) + var payload struct { + DiskOperate *struct { + NewDisks *struct { + MountNewCreateDisks []struct { + StorageConfig *models.StorageConfig `json:"storage_config,omitempty"` + } `json:"mount_new_create_disks,omitempty"` + } `json:"new_disks,omitempty"` + } `json:"disk_operate,omitempty"` + } + g.Expect(json.Unmarshal(serialized, &payload)).To(gomega.Succeed()) + g.Expect(payload.DiskOperate).NotTo(gomega.BeNil()) + g.Expect(payload.DiskOperate.NewDisks.MountNewCreateDisks).To(gomega.HaveLen(1)) + g.Expect(payload.DiskOperate.NewDisks.MountNewCreateDisks[0].StorageConfig).NotTo(gomega.BeNil()) + g.Expect(*payload.DiskOperate.NewDisks.MountNewCreateDisks[0].StorageConfig.StorageClusterID).To(gomega.Equal("storage-cluster")) + }) +} + +func newTestCreateVMFromTemplateParams(t *testing.T, clusterType infrav1.ElfClusterType, config *models.StorageConfig) *models.VMCreateVMFromContentLibraryTemplateParams { + t.Helper() + + service := &TowerVMService{} + elfCluster := &infrav1.ElfCluster{ + Spec: infrav1.ElfClusterSpec{ClusterType: clusterType}, + } + elfMachine := &infrav1.ElfMachine{ + ObjectMeta: metav1.ObjectMeta{Name: "machine"}, + Spec: infrav1.ElfMachineSpec{ + NumCPUs: 1, + NumCoresPerSocket: 1, + MemoryMiB: 1024, + OSType: "LINUX", + }, + } + template := &models.ContentLibraryVMTemplate{ + ID: TowerString("template"), + } + if clusterType == infrav1.ElfClusterTypeStretched { + template.VMDisks = []*models.NestedContentLibraryVMTemplateDisk{ + { + Type: models.NewVMDiskType(models.VMDiskTypeDISK), + Boot: TowerInt32(0), + Bus: models.NewBus(models.BusVIRTIO), + Index: TowerInt32(0), + Size: TowerInt64(1024), + }, + } + } + + params, err := service.createVMFromTemplateParams( + elfCluster, + elfMachine, + &models.Cluster{ID: TowerString("compute-cluster")}, + template, + &CloneVMInfo{StorageConfig: config}, + ) + if err != nil { + t.Fatalf("createVMFromTemplateParams() error = %v", err) + } + + return params +} diff --git a/pkg/session/tower.go b/pkg/session/tower.go deleted file mode 100644 index 824939f9..00000000 --- a/pkg/session/tower.go +++ /dev/null @@ -1,175 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package session - -import ( - goctx "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "net/http" - "sync" - "time" - - "github.com/go-logr/logr" - httptransport "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" - "github.com/pkg/errors" - towerclient "github.com/smartxworks/cloudtower-go-sdk/v2/client" - "github.com/smartxworks/cloudtower-go-sdk/v2/client/user" - "github.com/smartxworks/cloudtower-go-sdk/v2/models" - ctrl "sigs.k8s.io/controller-runtime" - - infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" -) - -var lastGCTime = time.Now() -var gcMinInterval = 10 * time.Minute -var sessionIdleTime = 10 * time.Minute - -// global Session map against sessionKeys -// in map[sessionKey]cacheItem. -var sessionCache sync.Map - -type cacheItem struct { - LastUsedTime time.Time - Session *TowerSession -} - -type TowerSession struct { - *towerclient.Cloudtower -} - -// GetOrCreate gets a cached session or creates a new one if one does not -// already exist. -func GetOrCreate(ctx goctx.Context, tower infrav1.Tower) (*TowerSession, error) { - logger := ctrl.LoggerFrom(ctx).WithName("session").WithValues("server", tower.Server, "username", tower.Username, "source", tower.AuthMode) - - defer func() { - if lastGCTime.Add(gcMinInterval).Before(time.Now()) { - cleanupSessionCache(logger) - lastGCTime = time.Now() - } - }() - - sessionKey := getSessionKey(&tower) - if value, ok := sessionCache.Load(sessionKey); ok { - item := value.(*cacheItem) - item.LastUsedTime = time.Now() - logger.V(3).Info("found active cached tower client session") - - return item.Session, nil - } - - client, err := createTowerClient(httptransport.TLSClientOptions{ - InsecureSkipVerify: tower.SkipTLSVerify, - }, towerclient.ClientConfig{ - Host: tower.Server, - BasePath: "/v2/api", - Schemes: []string{"https"}, - }, towerclient.UserConfig{ - Name: tower.Username, - Password: tower.Password, - Source: models.UserSource(tower.AuthMode), - }) - if err != nil { - return nil, errors.Wrap(err, "failed to create tower client") - } - - session := &TowerSession{client} - - // Cache the session. - sessionCache.Store(sessionKey, &cacheItem{LastUsedTime: time.Now(), Session: session}) - logger.V(3).Info("cached tower client session") - - return session, nil -} - -func createTowerClient(tlsOpts httptransport.TLSClientOptions, clientConfig towerclient.ClientConfig, userConfig towerclient.UserConfig) (*towerclient.Cloudtower, error) { - transport := httptransport.New(clientConfig.Host, clientConfig.BasePath, clientConfig.Schemes) - roundTripper, err := httptransport.TLSTransport(tlsOpts) - if err != nil { - return nil, err - } - - // For Arcfra vendor, we need to bypass the whitelist for AOC(CloudTower) - rtWithHeader := NewWithHeaderRoundTripper(roundTripper) - rtWithHeader.Set("x-bypass-whitelist", "true") //nolint:canonicalheader - transport.Transport = rtWithHeader - - client := towerclient.New(transport, strfmt.Default) - params := user.NewLoginParams() - params.WithTimeout(10 * time.Second) - params.RequestBody = &models.LoginInput{ - Username: &userConfig.Name, - Password: &userConfig.Password, - Source: userConfig.Source.Pointer(), - } - resp, err := client.User.Login(params) - if err != nil { - return nil, err - } - transport.DefaultAuthentication = httptransport.APIKeyAuth("Authorization", "header", *resp.Payload.Data.Token) - return client, nil -} - -func cleanupSessionCache(logger logr.Logger) { - sessionCache.Range(func(key interface{}, value interface{}) bool { - item := value.(*cacheItem) - if item.LastUsedTime.Add(sessionIdleTime).Before(time.Now()) { - sessionCache.Delete(key) - logger.V(3).Info(fmt.Sprintf("delete inactive tower client session %s from sessionCache", key)) - } - - return true - }) -} - -func getSessionKey(tower *infrav1.Tower) string { - encryptedTower := tower.DeepCopy() - sum256 := sha256.Sum256([]byte(tower.Password)) - encryptedTower.Password = hex.EncodeToString(sum256[:]) - - return fmt.Sprintf("%v", encryptedTower) -} - -type withHeaderRoundTripper struct { - http.Header - - rt http.RoundTripper -} - -func NewWithHeaderRoundTripper(rt http.RoundTripper) withHeaderRoundTripper { - if rt == nil { - rt = http.DefaultTransport - } - - return withHeaderRoundTripper{Header: make(http.Header), rt: rt} -} - -func (h withHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if len(h.Header) == 0 { - return h.rt.RoundTrip(req) - } - - req = req.Clone(req.Context()) - for k, v := range h.Header { - req.Header[k] = v - } - - return h.rt.RoundTrip(req) -} diff --git a/pkg/session/tower_test.go b/pkg/session/tower_test.go deleted file mode 100644 index a08c25d2..00000000 --- a/pkg/session/tower_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package session - -import ( - goctx "context" - "testing" - "time" - - "github.com/onsi/gomega" - - infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" -) - -func TestGetOrCreate(t *testing.T) { - g := gomega.NewGomegaWithT(t) - - t.Run("should get cached session and clear inactive session", func(t *testing.T) { - lastGCTime = lastGCTime.Add(-gcMinInterval) - tower := infrav1.Tower{Server: "127.0.0.1", Username: "tower", Password: "tower"} - inactiveTower := tower.DeepCopy() - inactiveTower.Username = "inactive" - invalidTower := tower.DeepCopy() - invalidTower.Username = "invalid" - - sessionKey := getSessionKey(&tower) - cachedSession := &TowerSession{} - sessionCache.Store(sessionKey, &cacheItem{Session: cachedSession}) - inactiveSessionKey := getSessionKey(inactiveTower) - sessionCache.Store(inactiveSessionKey, &cacheItem{Session: &TowerSession{}, LastUsedTime: time.Now().Add(-sessionIdleTime)}) - - session, err := GetOrCreate(goctx.Background(), tower) - g.Expect(err).ToNot(gomega.HaveOccurred()) - g.Expect(session).To(gomega.Equal(cachedSession)) - - _, ok := sessionCache.Load(inactiveSessionKey) - g.Expect(ok).To(gomega.BeFalse()) - - session, err = GetOrCreate(goctx.Background(), *invalidTower) - g.Expect(session).To(gomega.BeNil()) - g.Expect(err).To(gomega.HaveOccurred()) - }) -} diff --git a/pkg/util/annotations/helpers.go b/pkg/util/annotations/helpers.go index 9007c1fc..818d2f75 100644 --- a/pkg/util/annotations/helpers.go +++ b/pkg/util/annotations/helpers.go @@ -18,7 +18,7 @@ package annotations import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/cluster-api/util/annotations" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" diff --git a/pkg/util/capi/annotations.go b/pkg/util/capi/annotations.go new file mode 100644 index 00000000..cc86c06a --- /dev/null +++ b/pkg/util/capi/annotations.go @@ -0,0 +1,47 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package capi + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) + +// Copied from https://github.com/kubernetes-sigs/cluster-api/blob/release-1.10/util/annotations/helpers.go. + +// IsPaused returns true if the Cluster is paused or the object has the `paused` annotation. +func IsPaused(cluster *clusterv1.Cluster, o metav1.Object) bool { + if cluster.Spec.Paused { + return true + } + return HasPaused(o) +} + +// HasPaused returns true if the object has the `paused` annotation. +func HasPaused(o metav1.Object) bool { + return hasAnnotation(o, clusterv1.PausedAnnotation) +} + +// hasAnnotation returns true if the object has the specified annotation. +func hasAnnotation(o metav1.Object, annotation string) bool { + annotations := o.GetAnnotations() + if annotations == nil { + return false + } + _, ok := annotations[annotation] + return ok +} diff --git a/pkg/util/capi/collections/machine_collection.go b/pkg/util/capi/collections/machine_collection.go new file mode 100644 index 00000000..a42f2c18 --- /dev/null +++ b/pkg/util/capi/collections/machine_collection.go @@ -0,0 +1,284 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Modified copy of k8s.io/apimachinery/pkg/util/sets/int64.go +// Modifications +// - int64 became *clusterv1.Machine +// - Empty type is removed +// - Sortable data type is removed in favor of util.MachinesByCreationTimestamp +// - nil checks added to account for the pointer +// - Added Filter, AnyFilter, and Oldest methods +// - Added FromMachineList initializer +// - Updated Has to also check for equality of Machines +// - Removed unused methods + +package collections + +import ( + "sort" + + "github.com/blang/semver/v4" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" + "sigs.k8s.io/cluster-api/util/version" +) + +// Copied from https://github.com/kubernetes-sigs/cluster-api/blob/release-1.10/util/collections/machine_collection.go. + +// Machines is a set of Machines. +type Machines map[string]*clusterv1.Machine + +// MachinesByVersion sorts the list of Machine by spec.version, using their names as tie breaker. +// machines with no version are placed lower in the order. +type machinesByVersion []*clusterv1.Machine + +func (v machinesByVersion) Len() int { return len(v) } +func (v machinesByVersion) Swap(i, j int) { v[i], v[j] = v[j], v[i] } +func (v machinesByVersion) Less(i, j int) bool { + vi, _ := semver.ParseTolerant(*v[i].Spec.Version) + vj, _ := semver.ParseTolerant(*v[j].Spec.Version) + comp := version.Compare(vi, vj, version.WithBuildTags()) + if comp == 0 { + return v[i].Name < v[j].Name + } + return comp == -1 +} + +// machinesByCreationTimestamp sorts a list of Machine by creation timestamp, using their names as a tie breaker. +type machinesByCreationTimestamp []*clusterv1.Machine + +func (o machinesByCreationTimestamp) Len() int { return len(o) } +func (o machinesByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } +func (o machinesByCreationTimestamp) Less(i, j int) bool { + if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) { + return o[i].Name < o[j].Name + } + return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) +} + +// machinesByDeletionTimestamp sorts a list of Machines by deletion timestamp, using their names as a tie breaker. +// Machines without DeletionTimestamp go after machines with this field set. +type machinesByDeletionTimestamp []*clusterv1.Machine + +func (o machinesByDeletionTimestamp) Len() int { return len(o) } +func (o machinesByDeletionTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } +func (o machinesByDeletionTimestamp) Less(i, j int) bool { + if o[i].DeletionTimestamp == nil && o[j].DeletionTimestamp == nil { + return o[i].Name < o[j].Name + } + + if o[i].DeletionTimestamp == nil { + return false + } + + if o[j].DeletionTimestamp == nil { + return true + } + + if o[i].DeletionTimestamp.Equal(o[j].DeletionTimestamp) { + return o[i].Name < o[j].Name + } + return o[i].DeletionTimestamp.Before(o[j].DeletionTimestamp) +} + +// New creates an empty Machines. +func New() Machines { + return make(Machines) +} + +// FromMachines creates a Machines from a list of values. +func FromMachines(machines ...*clusterv1.Machine) Machines { + ss := make(Machines, len(machines)) + ss.Insert(machines...) + return ss +} + +// FromMachineList creates a Machines from the given MachineList. +func FromMachineList(machineList *clusterv1.MachineList) Machines { + ss := make(Machines, len(machineList.Items)) + for i := range machineList.Items { + ss.Insert(&machineList.Items[i]) + } + return ss +} + +// ToMachineList creates a MachineList from the given Machines. +func ToMachineList(machines Machines) clusterv1.MachineList { + ml := clusterv1.MachineList{} + for _, m := range machines { + ml.Items = append(ml.Items, *m) + } + return ml +} + +// Has return true when the collection has the given machine. +func (s Machines) Has(machine *clusterv1.Machine) bool { + for _, m := range s { + if m.Name == machine.Name && m.Namespace == machine.Namespace { + return true + } + } + return false +} + +// Insert adds items to the set. +func (s Machines) Insert(machines ...*clusterv1.Machine) { + for i := range machines { + if machines[i] != nil { + m := machines[i] + s[m.Name] = m + } + } +} + +// Difference returns a copy without machines that are in the given collection. +func (s Machines) Difference(machines Machines) Machines { + return s.Filter(func(m *clusterv1.Machine) bool { + _, found := machines[m.Name] + return !found + }) +} + +// SortedByCreationTimestamp returns the machines sorted by creation timestamp. +func (s Machines) SortedByCreationTimestamp() []*clusterv1.Machine { + res := make(machinesByCreationTimestamp, 0, len(s)) + for _, value := range s { + res = append(res, value) + } + sort.Sort(res) + return res +} + +// SortedByDeletionTimestamp returns the machines sorted by deletion timestamp. +func (s Machines) SortedByDeletionTimestamp() []*clusterv1.Machine { + res := make(machinesByDeletionTimestamp, 0, len(s)) + for _, value := range s { + res = append(res, value) + } + sort.Sort(res) + return res +} + +// UnsortedList returns the slice with contents in random order. +func (s Machines) UnsortedList() []*clusterv1.Machine { + res := make([]*clusterv1.Machine, 0, len(s)) + for _, value := range s { + res = append(res, value) + } + return res +} + +// Len returns the size of the set. +func (s Machines) Len() int { + return len(s) +} + +// newFilteredMachineCollection creates a Machines from a filtered list of values. +func newFilteredMachineCollection(filter Func, machines ...*clusterv1.Machine) Machines { + ss := make(Machines, len(machines)) + for i := range machines { + m := machines[i] + if filter(m) { + ss.Insert(m) + } + } + return ss +} + +// Filter returns a Machines containing only the Machines that match all of the given MachineFilters. +func (s Machines) Filter(filters ...Func) Machines { + return newFilteredMachineCollection(And(filters...), s.UnsortedList()...) +} + +// AnyFilter returns a Machines containing only the Machines that match any of the given MachineFilters. +func (s Machines) AnyFilter(filters ...Func) Machines { + return newFilteredMachineCollection(Or(filters...), s.UnsortedList()...) +} + +// Oldest returns the Machine with the oldest CreationTimestamp. +func (s Machines) Oldest() *clusterv1.Machine { + if len(s) == 0 { + return nil + } + return s.SortedByCreationTimestamp()[0] +} + +// Newest returns the Machine with the most recent CreationTimestamp. +func (s Machines) Newest() *clusterv1.Machine { + if len(s) == 0 { + return nil + } + return s.SortedByCreationTimestamp()[len(s)-1] +} + +// OldestDeletionTimestamp returns the Machine with the oldest DeletionTimestamp. +func (s Machines) OldestDeletionTimestamp() *clusterv1.Machine { + if len(s) == 0 { + return nil + } + return s.SortedByDeletionTimestamp()[0] +} + +// DeepCopy returns a deep copy. +func (s Machines) DeepCopy() Machines { + result := make(Machines, len(s)) + for _, m := range s { + result.Insert(m.DeepCopy()) + } + return result +} + +// ConditionGetters returns the slice with machines converted into conditions.Getter. +func (s Machines) ConditionGetters() []conditions.Getter { + res := make([]conditions.Getter, 0, len(s)) + for _, v := range s { + value := *v + res = append(res, &value) + } + return res +} + +// Names returns a slice of the names of each machine in the collection. +// Useful for logging and test assertions. +func (s Machines) Names() []string { + names := make([]string, 0, s.Len()) + for _, m := range s { + names = append(names, m.Name) + } + return names +} + +// SortedByVersion returns the machines sorted by version. +func (s Machines) sortedByVersion() []*clusterv1.Machine { + res := make(machinesByVersion, 0, len(s)) + for _, value := range s { + res = append(res, value) + } + sort.Sort(res) + return res +} + +// LowestVersion returns the lowest version among all the machine with +// defined versions. If no machine has a defined version it returns an +// empty string. +func (s Machines) LowestVersion() *string { + machines := s.Filter(WithVersion()) + if len(machines) == 0 { + return nil + } + m := machines.sortedByVersion()[0] + return m.Spec.Version +} diff --git a/pkg/util/capi/collections/machine_collection_test.go b/pkg/util/capi/collections/machine_collection_test.go new file mode 100644 index 00000000..860ad0f2 --- /dev/null +++ b/pkg/util/capi/collections/machine_collection_test.go @@ -0,0 +1,191 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package collections + +import ( + "testing" + "time" + + "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) + +func TestMachineCollection(t *testing.T) { + t.Run("SortedByCreationTimestamp", func(t *testing.T) { + t.Run("should return the same number of machines as are in the collection", func(t *testing.T) { + g := gomega.NewWithT(t) + collection := machines() + sortedMachines := collection.SortedByCreationTimestamp() + g.Expect(sortedMachines).To(gomega.HaveLen(len(collection))) + g.Expect(sortedMachines[0].Name).To(gomega.Equal("machine-1")) + g.Expect(sortedMachines[len(sortedMachines)-1].Name).To(gomega.Equal("machine-5")) + g.Expect(collection.Oldest().Name).To(gomega.Equal("machine-1")) + }) + }) + t.Run("SortedByDeletionTimestamp", func(t *testing.T) { + t.Run("should return the same number of machines as are in the collection", func(t *testing.T) { + g := gomega.NewWithT(t) + collection := machines() + // Adding Machines without deletionTimestamp. + collection["machine-6"] = machine("machine-6") + collection["machine-7"] = machine("machine-7") + collection["machine-8"] = machine("machine-8") + + sortedMachines := collection.SortedByDeletionTimestamp() + g.Expect(sortedMachines).To(gomega.HaveLen(len(collection))) + g.Expect(sortedMachines[0].Name).To(gomega.Equal("machine-1")) + g.Expect(sortedMachines[len(sortedMachines)-1].Name).To(gomega.Equal("machine-8")) + g.Expect(collection.OldestDeletionTimestamp().Name).To(gomega.Equal("machine-1")) + }) + }) + t.Run("Difference", func(t *testing.T) { + t.Run("should return the collection with elements of the second collection removed", func(t *testing.T) { + g := gomega.NewWithT(t) + collection := machines() + c2 := collection.Filter(func(m *clusterv1.Machine) bool { + return m.Name != "machine-1" + }) + c3 := collection.Difference(c2) + // does not mutate + g.Expect(collection.Names()).To(gomega.ContainElement("machine-1")) + g.Expect(c3.Names()).To(gomega.ConsistOf("machine-1")) + }) + }) + t.Run("Names", func(t *testing.T) { + t.Run("should return a slice of names of each machine in the collection", func(t *testing.T) { + g := gomega.NewWithT(t) + g.Expect(New().Names()).To(gomega.BeEmpty()) + g.Expect(FromMachines(machine("1"), machine("2")).Names()).To(gomega.ConsistOf("1", "2")) + }) + }) +} + +func TestMachinesLowestVersion(t *testing.T) { + tests := []struct { + name string + machines Machines + expected *string + }{ + { + name: "return empty for empty machines collection", + machines: New(), + expected: nil, + }, + { + name: "return empty if machines dont have version", + machines: func() Machines { + machines := New() + machines.Insert(&clusterv1.Machine{}) + return machines + }(), + expected: nil, + }, + { + name: "return lowest version from machines", + machines: func() Machines { + machines := New() + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-1"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-2"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.19.8"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To(""), + }}) + return machines + }(), + expected: ptr.To("1.19.8"), + }, + { + name: "return lowest version from machines with pre release versions", + machines: func() Machines { + machines := New() + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-1"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20.1"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-2"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20.1-alpha.1"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To(""), + }}) + return machines + }(), + expected: ptr.To("1.20.1-alpha.1"), + }, + { + name: "return lowest version from machines with build identifier versions", + machines: func() Machines { + machines := New() + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-1"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20.1+xyz.2"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-2"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20.1+xyz.1"), + }}) + machines.Insert(&clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}, Spec: clusterv1.MachineSpec{ + Version: ptr.To(""), + }}) + return machines + }(), + expected: ptr.To("1.20.1+xyz.1"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := gomega.NewWithT(t) + g.Expect(tt.machines.LowestVersion()).To(gomega.Equal(tt.expected)) + }) + } +} + +/* Helper functions to build machine objects for tests. */ + +type machineOpt func(*clusterv1.Machine) + +func withTimestamps(timestamp metav1.Time) machineOpt { + return func(m *clusterv1.Machine) { + m.CreationTimestamp = timestamp + m.DeletionTimestamp = ×tamp + } +} + +func machine(name string, opts ...machineOpt) *clusterv1.Machine { + m := &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + for _, opt := range opts { + opt(m) + } + return m +} + +func machines() Machines { + return Machines{ + "machine-4": machine("machine-4", withTimestamps(metav1.Time{Time: time.Date(2018, 04, 02, 03, 04, 05, 06, time.UTC)})), + "machine-5": machine("machine-5", withTimestamps(metav1.Time{Time: time.Date(2018, 05, 02, 03, 04, 05, 06, time.UTC)})), + "machine-2": machine("machine-2", withTimestamps(metav1.Time{Time: time.Date(2018, 02, 02, 03, 04, 05, 06, time.UTC)})), + "machine-1": machine("machine-1", withTimestamps(metav1.Time{Time: time.Date(2018, 01, 02, 03, 04, 05, 06, time.UTC)})), + "machine-3": machine("machine-3", withTimestamps(metav1.Time{Time: time.Date(2018, 03, 02, 03, 04, 05, 06, time.UTC)})), + } +} diff --git a/pkg/util/capi/collections/machine_filters.go b/pkg/util/capi/collections/machine_filters.go new file mode 100644 index 00000000..cb129c49 --- /dev/null +++ b/pkg/util/capi/collections/machine_filters.go @@ -0,0 +1,74 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package collections + +import ( + "github.com/blang/semver/v4" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) + +// Copied from https://github.com/kubernetes-sigs/cluster-api/blob/release-1.10/util/collections/machine_filters.go. + +// Func is the functon definition for a filter. +type Func func(machine *clusterv1.Machine) bool + +// And returns a filter that returns true if all of the given filters returns true. +func And(filters ...Func) Func { + return func(machine *clusterv1.Machine) bool { + for _, f := range filters { + if !f(machine) { + return false + } + } + return true + } +} + +// Or returns a filter that returns true if any of the given filters returns true. +func Or(filters ...Func) Func { + return func(machine *clusterv1.Machine) bool { + for _, f := range filters { + if f(machine) { + return true + } + } + return false + } +} + +// Not returns a filter that returns the opposite of the given filter. +func Not(mf Func) Func { + return func(machine *clusterv1.Machine) bool { + return !mf(machine) + } +} + +// WithVersion returns a filter to find machine that have a non empty and valid version. +func WithVersion() Func { + return func(machine *clusterv1.Machine) bool { + if machine == nil { + return false + } + if machine.Spec.Version == nil { + return false + } + if _, err := semver.ParseTolerant(*machine.Spec.Version); err != nil { + return false + } + return true + } +} diff --git a/pkg/util/capi/collections/machine_filters_test.go b/pkg/util/capi/collections/machine_filters_test.go new file mode 100644 index 00000000..7ef4aa70 --- /dev/null +++ b/pkg/util/capi/collections/machine_filters_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package collections + +import ( + "testing" + + "github.com/onsi/gomega" + "k8s.io/utils/ptr" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) + +func falseFilter(_ *clusterv1.Machine) bool { + return false +} + +func trueFilter(_ *clusterv1.Machine) bool { + return true +} + +func TestNot(t *testing.T) { + t.Run("returns false given a machine filter that returns true", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(Not(trueFilter)(m)).To(gomega.BeFalse()) + }) + t.Run("returns true given a machine filter that returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(Not(falseFilter)(m)).To(gomega.BeTrue()) + }) +} + +func TestAnd(t *testing.T) { + t.Run("returns true if both given machine filters return true", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(And(trueFilter, trueFilter)(m)).To(gomega.BeTrue()) + }) + t.Run("returns false if either given machine filter returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(And(trueFilter, falseFilter)(m)).To(gomega.BeFalse()) + }) +} + +func TestOr(t *testing.T) { + t.Run("returns true if either given machine filters return true", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(Or(trueFilter, falseFilter)(m)).To(gomega.BeTrue()) + }) + t.Run("returns false if both given machine filter returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + m := &clusterv1.Machine{} + g.Expect(Or(falseFilter, falseFilter)(m)).To(gomega.BeFalse()) + }) +} + +func TestWithVersion(t *testing.T) { + t.Run("nil machine returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + g.Expect(WithVersion()(nil)).To(gomega.BeFalse()) + }) + + t.Run("nil machine.Spec.Version returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + machine := &clusterv1.Machine{ + Spec: clusterv1.MachineSpec{ + Version: nil, + }, + } + g.Expect(WithVersion()(machine)).To(gomega.BeFalse()) + }) + + t.Run("empty machine.Spec.Version returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + machine := &clusterv1.Machine{ + Spec: clusterv1.MachineSpec{ + Version: ptr.To(""), + }, + } + g.Expect(WithVersion()(machine)).To(gomega.BeFalse()) + }) + + t.Run("invalid machine.Spec.Version returns false", func(t *testing.T) { + g := gomega.NewWithT(t) + machine := &clusterv1.Machine{ + Spec: clusterv1.MachineSpec{ + Version: ptr.To("1..20"), + }, + } + g.Expect(WithVersion()(machine)).To(gomega.BeFalse()) + }) + + t.Run("valid machine.Spec.Version returns true", func(t *testing.T) { + g := gomega.NewWithT(t) + machine := &clusterv1.Machine{ + Spec: clusterv1.MachineSpec{ + Version: ptr.To("1.20"), + }, + } + g.Expect(WithVersion()(machine)).To(gomega.BeTrue()) + }) +} diff --git a/pkg/util/capi/kubeconfig/kubeconfig.go b/pkg/util/capi/kubeconfig/kubeconfig.go new file mode 100644 index 00000000..b05cd017 --- /dev/null +++ b/pkg/util/capi/kubeconfig/kubeconfig.go @@ -0,0 +1,248 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kubeconfig implements utilities for working with kubeconfigs. +package kubeconfig + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + "net/url" + "time" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + capiutil "sigs.k8s.io/cluster-api/util" + "sigs.k8s.io/cluster-api/util/certs" + "sigs.k8s.io/cluster-api/util/secret" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + // ErrDependentCertificateNotFound signals that a CA secret could not be found. + ErrDependentCertificateNotFound = errors.New("could not find secret ca") +) + +// FromSecret fetches the Kubeconfig for a Cluster. +func FromSecret(ctx context.Context, c client.Reader, cluster client.ObjectKey) ([]byte, error) { + out, err := secret.Get(ctx, c, cluster, secret.Kubeconfig) + if err != nil { + return nil, err + } + return toKubeconfigBytes(out) +} + +// New creates a new Kubeconfig using the cluster name and specified endpoint. +func New(clusterName, endpoint string, caCert *x509.Certificate, caKey crypto.Signer) (*api.Config, error) { + cfg := &certs.Config{ + CommonName: "kubernetes-admin", + Organization: []string{"system:masters"}, + Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + clientKey, err := certs.NewPrivateKey() + if err != nil { + return nil, errors.Wrap(err, "unable to create private key") + } + + clientCert, err := cfg.NewSignedCert(clientKey, caCert, caKey) + if err != nil { + return nil, errors.Wrap(err, "unable to sign certificate") + } + + userName := clusterName + "-admin" + contextName := fmt.Sprintf("%s@%s", userName, clusterName) + + return &api.Config{ + Clusters: map[string]*api.Cluster{ + clusterName: { + Server: endpoint, + CertificateAuthorityData: certs.EncodeCertPEM(caCert), + }, + }, + Contexts: map[string]*api.Context{ + contextName: { + Cluster: clusterName, + AuthInfo: userName, + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + userName: { + ClientKeyData: certs.EncodePrivateKeyPEM(clientKey), + ClientCertificateData: certs.EncodeCertPEM(clientCert), + }, + }, + CurrentContext: contextName, + }, nil +} + +// CreateSecret creates the Kubeconfig secret for the given cluster. +func CreateSecret(ctx context.Context, c client.Client, cluster *clusterv1.Cluster) error { + name := capiutil.ObjectKey(cluster) + return CreateSecretWithOwner(ctx, c, name, cluster.Spec.ControlPlaneEndpoint.String(), metav1.OwnerReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: cluster.Name, + UID: cluster.UID, + }) +} + +// CreateSecretWithOwner creates the Kubeconfig secret for the given cluster name, namespace, endpoint, and owner reference. +func CreateSecretWithOwner(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string, owner metav1.OwnerReference) error { + server, err := url.JoinPath("https://", endpoint) + if err != nil { + return err + } + out, err := generateKubeconfig(ctx, c, clusterName, server) + if err != nil { + return err + } + + return c.Create(ctx, GenerateSecretWithOwner(clusterName, out, owner)) +} + +// GenerateSecret returns a Kubernetes secret for the given Cluster and kubeconfig data. +func GenerateSecret(cluster *clusterv1.Cluster, data []byte) *corev1.Secret { + name := capiutil.ObjectKey(cluster) + return GenerateSecretWithOwner(name, data, metav1.OwnerReference{ + APIVersion: clusterv1.GroupVersion.String(), + Kind: "Cluster", + Name: cluster.Name, + UID: cluster.UID, + }) +} + +// GenerateSecretWithOwner returns a Kubernetes secret for the given Cluster name, namespace, kubeconfig data, and ownerReference. +func GenerateSecretWithOwner(clusterName client.ObjectKey, data []byte, owner metav1.OwnerReference) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secret.Name(clusterName.Name, secret.Kubeconfig), + Namespace: clusterName.Namespace, + Labels: map[string]string{ + clusterv1.ClusterNameLabel: clusterName.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + owner, + }, + }, + Data: map[string][]byte{ + secret.KubeconfigDataName: data, + }, + Type: clusterv1.ClusterSecretType, + } +} + +// NeedsClientCertRotation returns whether any of the Kubeconfig secret's client certificates will expire before the given threshold. +func NeedsClientCertRotation(configSecret *corev1.Secret, threshold time.Duration) (bool, error) { + now := time.Now() + + data, err := toKubeconfigBytes(configSecret) + if err != nil { + return false, err + } + + config, err := clientcmd.Load(data) + if err != nil { + return false, errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config") + } + + for _, authInfo := range config.AuthInfos { + cert, err := certs.DecodeCertPEM(authInfo.ClientCertificateData) + if err != nil { + return false, errors.Wrap(err, "failed to decode kubeconfig client certificate") + } + if cert.NotAfter.Sub(now) < threshold { + return true, nil + } + } + + return false, nil +} + +// RegenerateSecret creates and stores a new Kubeconfig in the given secret. +func RegenerateSecret(ctx context.Context, c client.Client, configSecret *corev1.Secret) error { + clusterName, _, err := secret.ParseSecretName(configSecret.Name) + if err != nil { + return errors.Wrap(err, "failed to parse secret name") + } + data, err := toKubeconfigBytes(configSecret) + if err != nil { + return err + } + + config, err := clientcmd.Load(data) + if err != nil { + return errors.Wrap(err, "failed to convert kubeconfig Secret into a clientcmdapi.Config") + } + endpoint := config.Clusters[clusterName].Server + key := client.ObjectKey{Name: clusterName, Namespace: configSecret.Namespace} + out, err := generateKubeconfig(ctx, c, key, endpoint) + if err != nil { + return err + } + configSecret.Data[secret.KubeconfigDataName] = out + return c.Update(ctx, configSecret) +} + +func generateKubeconfig(ctx context.Context, c client.Client, clusterName client.ObjectKey, endpoint string) ([]byte, error) { + clusterCA, err := secret.GetFromNamespacedName(ctx, c, clusterName, secret.ClusterCA) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, ErrDependentCertificateNotFound + } + return nil, err + } + + cert, err := certs.DecodeCertPEM(clusterCA.Data[secret.TLSCrtDataName]) + if err != nil { + return nil, errors.Wrap(err, "failed to decode CA Cert") + } else if cert == nil { + return nil, errors.New("certificate not found in config") + } + + key, err := certs.DecodePrivateKeyPEM(clusterCA.Data[secret.TLSKeyDataName]) + if err != nil { + return nil, errors.Wrap(err, "failed to decode private key") + } else if key == nil { + return nil, errors.New("CA private key not found") + } + + cfg, err := New(clusterName.Name, endpoint, cert, key) + if err != nil { + return nil, errors.Wrap(err, "failed to generate a kubeconfig") + } + + out, err := clientcmd.Write(*cfg) + if err != nil { + return nil, errors.Wrap(err, "failed to serialize config to yaml") + } + return out, nil +} + +func toKubeconfigBytes(out *corev1.Secret) ([]byte, error) { + data, ok := out.Data[secret.KubeconfigDataName] + if !ok { + return nil, errors.Errorf("missing key %q in secret data", secret.KubeconfigDataName) + } + return data, nil +} diff --git a/pkg/util/capi/kubeconfig/testing.go b/pkg/util/capi/kubeconfig/testing.go new file mode 100644 index 00000000..6f71e4e3 --- /dev/null +++ b/pkg/util/capi/kubeconfig/testing.go @@ -0,0 +1,54 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeconfig + +import ( + "fmt" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" +) + +// FromEnvTestConfig returns a new Kubeconfig in byte form when running in envtest. +func FromEnvTestConfig(cfg *rest.Config, cluster *clusterv1.Cluster) []byte { + contextName := fmt.Sprintf("%s@%s", cfg.Username, cluster.Name) + c := api.Config{ + Clusters: map[string]*api.Cluster{ + cluster.Name: { + Server: cfg.Host, + CertificateAuthorityData: cfg.CAData, + }, + }, + Contexts: map[string]*api.Context{ + contextName: { + Cluster: cluster.Name, + AuthInfo: cfg.Username, + }, + }, + AuthInfos: map[string]*api.AuthInfo{ + cfg.Username: { + ClientKeyData: cfg.KeyData, + ClientCertificateData: cfg.CertData, + }, + }, + CurrentContext: contextName, + } + data, _ := clientcmd.Write(c) + return data +} diff --git a/pkg/util/capi/util.go b/pkg/util/capi/util.go new file mode 100644 index 00000000..28d7dc2a --- /dev/null +++ b/pkg/util/capi/util.go @@ -0,0 +1,105 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package capi + +import ( + "context" + stderrors "errors" + "fmt" + + pkgerrors "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Copied from https://github.com/kubernetes-sigs/cluster-api/blob/release-1.10/util/util.go. + +var ( + // ErrNoCluster is returned when the cluster + // label could not be found on the object passed in. + ErrNoCluster = fmt.Errorf("no %q label present", clusterv1.ClusterNameLabel) + + // ErrUnstructuredFieldNotFound determines that a field + // in an unstructured object could not be found. + ErrUnstructuredFieldNotFound = stderrors.New("field not found") +) + +// GetClusterFromMetadata returns the Cluster object (if present) using the object metadata. +func GetClusterFromMetadata(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*clusterv1.Cluster, error) { + if obj.Labels[clusterv1.ClusterNameLabel] == "" { + return nil, pkgerrors.WithStack(ErrNoCluster) + } + return GetClusterByName(ctx, c, obj.Namespace, obj.Labels[clusterv1.ClusterNameLabel]) +} + +// GetOwnerCluster returns the Cluster object owning the current resource. +func GetOwnerCluster(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*clusterv1.Cluster, error) { + for _, ref := range obj.GetOwnerReferences() { + if ref.Kind != "Cluster" { + continue + } + gv, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + return nil, pkgerrors.WithStack(err) + } + if gv.Group == clusterv1.GroupVersion.Group { + return GetClusterByName(ctx, c, obj.Namespace, ref.Name) + } + } + return nil, nil +} + +// GetClusterByName finds and return a Cluster object using the specified params. +func GetClusterByName(ctx context.Context, c client.Client, namespace, name string) (*clusterv1.Cluster, error) { + cluster := &clusterv1.Cluster{} + key := client.ObjectKey{ + Namespace: namespace, + Name: name, + } + + if err := c.Get(ctx, key, cluster); err != nil { + return nil, pkgerrors.Wrapf(err, "failed to get Cluster/%s", name) + } + + return cluster, nil +} + +// GetOwnerMachine returns the Machine object owning the current resource. +func GetOwnerMachine(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*clusterv1.Machine, error) { + for _, ref := range obj.GetOwnerReferences() { + gv, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + return nil, err + } + if ref.Kind == "Machine" && gv.Group == clusterv1.GroupVersion.Group { + return GetMachineByName(ctx, c, obj.Namespace, ref.Name) + } + } + return nil, nil +} + +// GetMachineByName finds and return a Machine object using the specified params. +func GetMachineByName(ctx context.Context, c client.Client, namespace, name string) (*clusterv1.Machine, error) { + m := &clusterv1.Machine{} + key := client.ObjectKey{Name: name, Namespace: namespace} + if err := c.Get(ctx, key, m); err != nil { + return nil, err + } + return m, nil +} diff --git a/pkg/util/capi/util_test.go b/pkg/util/capi/util_test.go new file mode 100644 index 00000000..ffd4f103 --- /dev/null +++ b/pkg/util/capi/util_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package capi + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func newScheme(t *testing.T) *runtime.Scheme { + t.Helper() + + scheme := runtime.NewScheme() + g := NewWithT(t) + g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func TestGetOwnerClusterSuccessByName(t *testing.T) { + g := NewWithT(t) + + myCluster := &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster", + Namespace: metav1.NamespaceDefault, + }, + } + + c := fake.NewClientBuilder(). + WithScheme(newScheme(t)). + WithObjects(myCluster). + Build() + + objm := metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "Cluster", + APIVersion: clusterv1.GroupVersion.String(), + Name: "my-cluster", + }, + }, + Namespace: metav1.NamespaceDefault, + Name: "my-resource-owned-by-cluster", + } + cluster, err := GetOwnerCluster(t.Context(), c, objm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cluster).NotTo(BeNil()) + + // Make sure API version does not matter + objm.OwnerReferences[0].APIVersion = "cluster.x-k8s.io/v1alpha1234" + cluster, err = GetOwnerCluster(t.Context(), c, objm) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cluster).NotTo(BeNil()) +} diff --git a/pkg/util/kcp/kcp.go b/pkg/util/kcp/kcp.go index afbaffd3..74d25908 100644 --- a/pkg/util/kcp/kcp.go +++ b/pkg/util/kcp/kcp.go @@ -17,9 +17,9 @@ limitations under the License. */ import ( - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" ) // IsKCPInRollingUpdate returns whether KCP is in rolling update. diff --git a/pkg/util/kube.go b/pkg/util/kube.go index 0ebf502f..bb258db4 100644 --- a/pkg/util/kube.go +++ b/pkg/util/kube.go @@ -23,7 +23,7 @@ import ( "github.com/pkg/errors" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/cluster-api/util/kubeconfig" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/util/labels/helpers.go b/pkg/util/labels/helpers.go index 5d828c82..0b16499d 100644 --- a/pkg/util/labels/helpers.go +++ b/pkg/util/labels/helpers.go @@ -21,7 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" ) diff --git a/pkg/util/machine/kcp.go b/pkg/util/machine/kcp.go index 5ad54362..ce49c4af 100644 --- a/pkg/util/machine/kcp.go +++ b/pkg/util/machine/kcp.go @@ -23,8 +23,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" apitypes "k8s.io/apimachinery/pkg/types" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/util/machine/kcp_test.go b/pkg/util/machine/kcp_test.go index c75e7c00..56313e17 100644 --- a/pkg/util/machine/kcp_test.go +++ b/pkg/util/machine/kcp_test.go @@ -17,7 +17,6 @@ limitations under the License. package machine import ( - goctx "context" "testing" "github.com/onsi/gomega" @@ -27,20 +26,19 @@ import ( func TestGetKCPByMachine(t *testing.T) { g := gomega.NewGomegaWithT(t) - ctx := goctx.TODO() elfCluster, cluster := fake.NewClusterObjects() _, cpMachine := fake.NewMachineObjects(elfCluster, cluster) kubeadmCP := fake.NewKCP() fake.ToCPMachine(cpMachine, kubeadmCP) ctrlMgrCtx := fake.NewControllerManagerContext(kubeadmCP) t.Run("should return kcp", func(t *testing.T) { - kcp, err := GetKCPByMachine(ctx, ctrlMgrCtx.Client, cpMachine) + kcp, err := GetKCPByMachine(t.Context(), ctrlMgrCtx.Client, cpMachine) g.Expect(err).ToNot(gomega.HaveOccurred()) g.Expect(kcp.Name).To(gomega.Equal(kubeadmCP.Name)) }) _, workerMachine := fake.NewMachineObjects(elfCluster, cluster) t.Run("should panic when failed to get kcp name", func(t *testing.T) { - g.Expect(func() { _, _ = GetKCPByMachine(ctx, ctrlMgrCtx.Client, workerMachine) }).To(gomega.Panic()) + g.Expect(func() { _, _ = GetKCPByMachine(t.Context(), ctrlMgrCtx.Client, workerMachine) }).To(gomega.Panic()) }) } diff --git a/pkg/util/machine/machine.go b/pkg/util/machine/machine.go index b0ca87bf..1970282f 100644 --- a/pkg/util/machine/machine.go +++ b/pkg/util/machine/machine.go @@ -23,8 +23,8 @@ import ( "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - "sigs.k8s.io/cluster-api/util/conditions" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + conditions "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/conditions" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" diff --git a/pkg/util/machine/machine_test.go b/pkg/util/machine/machine_test.go index 9be3dd93..d3ed315d 100644 --- a/pkg/util/machine/machine_test.go +++ b/pkg/util/machine/machine_test.go @@ -17,7 +17,6 @@ limitations under the License. package machine import ( - goctx "context" "testing" "github.com/onsi/gomega" @@ -28,13 +27,12 @@ import ( func TestGetElfMachinesInCluster(t *testing.T) { g := gomega.NewGomegaWithT(t) - ctx := goctx.TODO() elfCluster, cluster := fake.NewClusterObjects() elfMachine, _ := fake.NewMachineObjects(elfCluster, cluster) ctrlMgrCtx := fake.NewControllerManagerContext(elfMachine) t.Run("should return ElfMachines", func(t *testing.T) { - elfMachines, err := GetElfMachinesInCluster(ctx, ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) + elfMachines, err := GetElfMachinesInCluster(t.Context(), ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) g.Expect(err).ToNot(gomega.HaveOccurred()) g.Expect(elfMachines).To(gomega.HaveLen(1)) }) @@ -42,7 +40,6 @@ func TestGetElfMachinesInCluster(t *testing.T) { func TestGetControlPlaneElfMachinesInCluster(t *testing.T) { g := gomega.NewGomegaWithT(t) - ctx := goctx.TODO() elfCluster, cluster := fake.NewClusterObjects() elfMachine1, _ := fake.NewMachineObjects(elfCluster, cluster) elfMachine2, _ := fake.NewMachineObjects(elfCluster, cluster) @@ -50,7 +47,7 @@ func TestGetControlPlaneElfMachinesInCluster(t *testing.T) { ctrlMgrCtx := fake.NewControllerManagerContext(elfMachine1, elfMachine2) t.Run("should return Control Plane ElfMachines", func(t *testing.T) { - elfMachines, err := GetControlPlaneElfMachinesInCluster(ctx, ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) + elfMachines, err := GetControlPlaneElfMachinesInCluster(t.Context(), ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) g.Expect(err).ToNot(gomega.HaveOccurred()) g.Expect(elfMachines).To(gomega.HaveLen(1)) g.Expect(elfMachines[0].Name).To(gomega.Equal(elfMachine1.Name)) @@ -238,7 +235,7 @@ func TestGetElfMachinesForMD(t *testing.T) { fake.ToWorkerMachine(elfMachine, md) elfMachine2, _ := fake.NewMachineObjects(elfCluster, cluster) ctrlMgrCtx := fake.NewControllerManagerContext(elfCluster, cluster, elfMachine, elfMachine2) - elfMachines, err := GetElfMachinesForMD(goctx.TODO(), ctrlMgrCtx.Client, cluster, md) + elfMachines, err := GetElfMachinesForMD(t.Context(), ctrlMgrCtx.Client, cluster, md) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(elfMachines).To(gomega.HaveLen(1)) g.Expect(elfMachines[0].Name).To(gomega.Equal(elfMachine.Name)) @@ -252,7 +249,7 @@ func TestGetControlPlaneMachinesForCluster(t *testing.T) { fake.ToCPMachine(machine, kcp) _, machine2 := fake.NewMachineObjects(elfCluster, cluster) ctrlMgrCtx := fake.NewControllerManagerContext(elfCluster, cluster, machine, machine2) - machines, err := GetControlPlaneMachinesForCluster(goctx.TODO(), ctrlMgrCtx.Client, cluster) + machines, err := GetControlPlaneMachinesForCluster(t.Context(), ctrlMgrCtx.Client, cluster) g.Expect(err).NotTo(gomega.HaveOccurred()) g.Expect(machines).To(gomega.HaveLen(1)) g.Expect(machines[0].Name).To(gomega.Equal(machine.Name)) diff --git a/pkg/util/machine/md.go b/pkg/util/machine/md.go index c41879cf..5b038522 100644 --- a/pkg/util/machine/md.go +++ b/pkg/util/machine/md.go @@ -20,7 +20,7 @@ import ( goctx "context" apitypes "k8s.io/apimachinery/pkg/types" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" labelutil "github.com/smartxworks/cluster-api-provider-elf/pkg/util/labels" diff --git a/pkg/util/machine/md_test.go b/pkg/util/machine/md_test.go index f0bfb34e..b4968422 100644 --- a/pkg/util/machine/md_test.go +++ b/pkg/util/machine/md_test.go @@ -17,18 +17,16 @@ limitations under the License. package machine import ( - goctx "context" "testing" "github.com/onsi/gomega" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/test/fake" ) func TestGetMDByMachine(t *testing.T) { g := gomega.NewGomegaWithT(t) - ctx := goctx.TODO() elfCluster, cluster := fake.NewClusterObjects() _, machine := fake.NewMachineObjects(elfCluster, cluster) machineDeployment := fake.NewMD() @@ -36,7 +34,7 @@ func TestGetMDByMachine(t *testing.T) { ctrlMgrCtx := fake.NewControllerManagerContext(machineDeployment) t.Run("should return md", func(t *testing.T) { - md, err := GetMDByMachine(ctx, ctrlMgrCtx.Client, machine) + md, err := GetMDByMachine(t.Context(), ctrlMgrCtx.Client, machine) g.Expect(err).ToNot(gomega.HaveOccurred()) g.Expect(md.Name).To(gomega.Equal(machineDeployment.Name)) }) @@ -44,7 +42,6 @@ func TestGetMDByMachine(t *testing.T) { func TestGetMDsForCluster(t *testing.T) { g := gomega.NewGomegaWithT(t) - ctx := goctx.TODO() _, cluster := fake.NewClusterObjects() md1 := fake.NewMD() md1.Labels = map[string]string{clusterv1.ClusterNameLabel: cluster.Name} @@ -52,7 +49,7 @@ func TestGetMDsForCluster(t *testing.T) { ctrlMgrCtx := fake.NewControllerManagerContext(md1, md2) t.Run("should return mds", func(t *testing.T) { - mds, err := GetMDsForCluster(ctx, ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) + mds, err := GetMDsForCluster(t.Context(), ctrlMgrCtx.Client, cluster.Namespace, cluster.Name) g.Expect(err).ToNot(gomega.HaveOccurred()) g.Expect(mds).To(gomega.HaveLen(1)) g.Expect(mds[0].Name).To(gomega.Equal(md1.Name)) diff --git a/pkg/util/md/md.go b/pkg/util/md/md.go index 31ffd939..0d815b74 100644 --- a/pkg/util/md/md.go +++ b/pkg/util/md/md.go @@ -18,7 +18,7 @@ limitations under the License. import ( intstrutil "k8s.io/apimachinery/pkg/util/intstr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" ) // IsMDInRollingUpdate returns whether MD is in rolling update. diff --git a/pkg/util/md/md_test.go b/pkg/util/md/md_test.go index 2c41b4b7..7c2b19ab 100644 --- a/pkg/util/md/md_test.go +++ b/pkg/util/md/md_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" ) /* diff --git a/templates/cluster-template.yaml b/templates/cluster-template.yaml index fa150da1..5e0693a4 100644 --- a/templates/cluster-template.yaml +++ b/templates/cluster-template.yaml @@ -27,11 +27,9 @@ metadata: spec: cluster: "${ELF_CLUSTER}" tower: - server: "${TOWER_SERVER}" - username: "${TOWER_USERNAME}" - password: "${TOWER_PASSWORD}" - authMode: ${TOWER_AUTH_MODE:=LOCAL} - skipTLSVerify: ${TOWER_SKIP_TLS_VERIFY:=false} + secretRef: + name: "${TOWER_SECRET_NAME}" + namespace: "${TOWER_SECRET_NAMESPACE}" controlPlaneEndpoint: host: "${CONTROL_PLANE_ENDPOINT_IP}" port: 6443 diff --git a/test/e2e/common.go b/test/e2e/common.go index 7fc7b89a..81499ff2 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" corev1 "k8s.io/api/core/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/test/framework" capiutil "sigs.k8s.io/cluster-api/util" ) @@ -61,7 +61,7 @@ func setupSpecNamespace(ctx context.Context, specName string, clusterProxy frame return namespace, cancelWatches } -func dumpSpecResourcesAndCleanup(ctx context.Context, specName string, clusterProxy framework.ClusterProxy, artifactFolder string, namespace *corev1.Namespace, cancelWatches context.CancelFunc, cluster *clusterv1.Cluster, intervalsGetter func(spec, key string) []interface{}, skipCleanup bool) { +func dumpSpecResourcesAndCleanup(ctx context.Context, specName string, clusterProxy framework.ClusterProxy, artifactFolder string, namespace *corev1.Namespace, cancelWatches context.CancelFunc, cluster *clusterv2.Cluster, intervalsGetter func(spec, key string) []interface{}, skipCleanup bool) { Byf("Dumping logs from the %q workload cluster", cluster.Name) // Dump all the logs from the workload cluster before deleting them. diff --git a/test/e2e/config/elf-dev.yaml b/test/e2e/config/elf-dev.yaml index 0fef9685..2bd35b12 100644 --- a/test/e2e/config/elf-dev.yaml +++ b/test/e2e/config/elf-dev.yaml @@ -11,27 +11,27 @@ # - from the CAPE repository root, `make test-e2e` to build the elf provider image and run e2e tests. images: - - name: k8s.gcr.io/cluster-api/cluster-api-controller:v1.8.10 + - name: k8s.gcr.io/cluster-api/cluster-api-controller:v1.11.11 loadBehavior: tryLoad - - name: k8s.gcr.io/cluster-api/kubeadm-bootstrap-controller:v1.8.10 + - name: k8s.gcr.io/cluster-api/kubeadm-bootstrap-controller:v1.11.11 loadBehavior: tryLoad - - name: k8s.gcr.io/cluster-api/kubeadm-control-plane-controller:v1.8.10 + - name: k8s.gcr.io/cluster-api/kubeadm-control-plane-controller:v1.11.11 loadBehavior: tryLoad - name: k8s.gcr.io/smartxworks/cape-manager:e2e loadBehavior: mustLoad - - name: quay.io/jetstack/cert-manager-cainjector:v1.18.1 + - name: quay.io/jetstack/cert-manager-cainjector:v1.20.1 loadBehavior: tryLoad - - name: quay.io/jetstack/cert-manager-webhook:v1.18.1 + - name: quay.io/jetstack/cert-manager-webhook:v1.20.1 loadBehavior: tryLoad - - name: quay.io/jetstack/cert-manager-controller:v1.18.1 + - name: quay.io/jetstack/cert-manager-controller:v1.20.1 loadBehavior: tryLoad providers: - name: cluster-api type: CoreProvider versions: - - name: v1.8.10 # Use manifest from source files - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.10/core-components.yaml" + - name: v1.11.11 # Use manifest from source files + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" type: "url" contract: v1beta1 files: @@ -45,8 +45,8 @@ providers: files: - sourcePath: "../data/capi/metadata.yaml" versions: - - name: v1.8.10 # Use manifest from source files - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.10/bootstrap-components.yaml" + - name: v1.11.11 # Use manifest from source files + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" type: "url" contract: v1beta1 files: @@ -60,8 +60,8 @@ providers: files: - sourcePath: "../data/capi/metadata.yaml" versions: - - name: v1.8.10 # Use manifest from source files - value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.10/control-plane-components.yaml" + - name: v1.11.11 # Use manifest from source files + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" type: "url" contract: v1beta1 files: diff --git a/test/e2e/controlplane_helpers.go b/test/e2e/controlplane_helpers.go index 0e422eff..df793a87 100644 --- a/test/e2e/controlplane_helpers.go +++ b/test/e2e/controlplane_helpers.go @@ -26,12 +26,11 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + controlplanev2 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/test/framework" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -39,8 +38,8 @@ import ( type ScaleAndWaitControlPlaneInput struct { ClusterProxy framework.ClusterProxy - Cluster *clusterv1.Cluster - ControlPlane *controlplanev1.KubeadmControlPlane + Cluster *clusterv2.Cluster + ControlPlane *controlplanev2.KubeadmControlPlane Replicas int32 WaitForControlPlane []interface{} } @@ -68,7 +67,7 @@ func ScaleAndWaitControlPlane(ctx context.Context, input ScaleAndWaitControlPlan nodeRefCount := 0 for _, machine := range machines { - if machine.Status.NodeRef != nil { + if machine.Status.NodeRef.IsDefined() { nodeRefCount++ } } @@ -84,8 +83,8 @@ func ScaleAndWaitControlPlane(ctx context.Context, input ScaleAndWaitControlPlan // UpgradeControlPlaneAndWaitForUpgradeInput is the input type for UpgradeControlPlaneAndWaitForUpgrade. type UpgradeControlPlaneAndWaitForUpgradeInput struct { ClusterProxy framework.ClusterProxy - Cluster *clusterv1.Cluster - ControlPlane *controlplanev1.KubeadmControlPlane + Cluster *clusterv2.Cluster + ControlPlane *controlplanev2.KubeadmControlPlane KubernetesUpgradeVersion string EtcdImageTag string DNSImageTag string @@ -112,7 +111,7 @@ func UpgradeControlPlaneAndWaitForUpgrade(ctx context.Context, input UpgradeCont Logf("Patching the new kubernetes version and infrastructure ref to KCP") // Retrieve infra object - infraRef := input.ControlPlane.Spec.MachineTemplate.InfrastructureRef + infraRef := input.ControlPlane.Spec.MachineTemplate.Spec.InfrastructureRef var elfMachineTemplate infrav1.ElfMachineTemplate elfMachineTemplateKey := client.ObjectKey{ Namespace: input.Cluster.Namespace, @@ -126,7 +125,7 @@ func UpgradeControlPlaneAndWaitForUpgrade(ctx context.Context, input UpgradeCont elfMachineTemplateData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&elfMachineTemplate) Expect(err).NotTo(HaveOccurred()) infraObj := &unstructured.Unstructured{Object: elfMachineTemplateData} - infraObj.SetGroupVersionKind(infraRef.GroupVersionKind()) + infraObj.SetGroupVersionKind(infrav1.GroupVersion.WithKind("ElfMachineTemplate")) // Creates a new infra object newInfraObj := infraObj @@ -144,19 +143,12 @@ func UpgradeControlPlaneAndWaitForUpgrade(ctx context.Context, input UpgradeCont oldVersion := input.ControlPlane.Spec.Version input.ControlPlane.Spec.Version = input.KubernetesUpgradeVersion - input.ControlPlane.Spec.MachineTemplate.InfrastructureRef.Name = newInfraObjName + input.ControlPlane.Spec.MachineTemplate.Spec.InfrastructureRef.Name = newInfraObjName // If the ClusterConfiguration is not specified, create an empty one. - if input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration == nil { - input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration = new(bootstrapv1.ClusterConfiguration) - } - - if input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local == nil { - input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = new(bootstrapv1.LocalEtcd) - } - - input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ImageMeta.ImageTag = input.EtcdImageTag - input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageMeta.ImageTag = input.DNSImageTag + // Note: In v1beta2, ClusterConfiguration is a value type, not a pointer. + input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ImageTag = input.EtcdImageTag + input.ControlPlane.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag = input.DNSImageTag Expect(patchHelper.Patch(ctx, input.ControlPlane)).To(Succeed()) diff --git a/test/e2e/ha_test.go b/test/e2e/ha_test.go index d87034a9..9bee7eaf 100644 --- a/test/e2e/ha_test.go +++ b/test/e2e/ha_test.go @@ -88,7 +88,7 @@ var _ = Describe("CAPE HA e2e test", func() { Logf("Shut down VM") ShutDownVM(ctx, ShutDownVMInput{ - UUID: machineutil.ConvertProviderIDToUUID(machines[0].Spec.ProviderID), + UUID: machineutil.ConvertProviderIDToUUID(ptr.To(machines[0].Spec.ProviderID)), VMService: vmService, WaitVMJobIntervals: e2eConfig.GetIntervals(specName, "wait-vm-job"), }) @@ -107,7 +107,7 @@ var _ = Describe("CAPE HA e2e test", func() { Byf("Wait for control plane nodes ready") Logf("Powering on VM") PowerOnVM(ctx, PowerOnVMInput{ - UUID: machineutil.ConvertProviderIDToUUID(machines[0].Spec.ProviderID), + UUID: machineutil.ConvertProviderIDToUUID(ptr.To(machines[0].Spec.ProviderID)), VMService: vmService, WaitVMJobIntervals: e2eConfig.GetIntervals(specName, "wait-vm-job"), }) diff --git a/test/e2e/machinedeployment_helpers.go b/test/e2e/machinedeployment_helpers.go index 827db798..3e159b40 100644 --- a/test/e2e/machinedeployment_helpers.go +++ b/test/e2e/machinedeployment_helpers.go @@ -23,10 +23,10 @@ import ( . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/cluster-api/test/framework" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" @@ -35,10 +35,10 @@ import ( // UpgradeMachineDeploymentsAndWaitInput is the input type for UpgradeMachineDeploymentsAndWait. type UpgradeMachineDeploymentsAndWaitInput struct { ClusterProxy framework.ClusterProxy - Cluster *clusterv1.Cluster + Cluster *clusterv2.Cluster UpgradeVersion string VMTemplate string - MachineDeployments []*clusterv1.MachineDeployment + MachineDeployments []*clusterv2.MachineDeployment WaitForMachinesToBeUpgraded []interface{} } @@ -69,7 +69,7 @@ func UpgradeMachineDeploymentsAndWait(ctx context.Context, input UpgradeMachineD elfMachineTemplateData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&elfMachineTemplate) Expect(err).NotTo(HaveOccurred()) infraObj := &unstructured.Unstructured{Object: elfMachineTemplateData} - infraObj.SetGroupVersionKind(infraRef.GroupVersionKind()) + infraObj.SetGroupVersionKind(infrav1.GroupVersion.WithKind("ElfMachineTemplate")) // Creates a new infra object newInfraObj := infraObj @@ -83,17 +83,17 @@ func UpgradeMachineDeploymentsAndWait(ctx context.Context, input UpgradeMachineD Expect(err).ToNot(HaveOccurred()) oldVersion := deployment.Spec.Template.Spec.Version - deployment.Spec.Template.Spec.Version = &input.UpgradeVersion + deployment.Spec.Template.Spec.Version = input.UpgradeVersion deployment.Spec.Template.Spec.InfrastructureRef.Name = newInfraObjName Expect(patchHelper.Patch(ctx, deployment)).To(Succeed()) Logf("Waiting for Kubernetes versions of machines in MachineDeployment %s/%s to be upgraded from %s to %s, VM template from %s to %s", - deployment.Namespace, deployment.Name, *oldVersion, input.UpgradeVersion, + deployment.Namespace, deployment.Name, oldVersion, input.UpgradeVersion, oldVMTemplate, input.VMTemplate) Logf("Waiting for Kubernetes versions of machines in MachineDeployment %s/%s to be upgraded from %s to %s", - deployment.Namespace, deployment.Name, *oldVersion, input.UpgradeVersion) + deployment.Namespace, deployment.Name, oldVersion, input.UpgradeVersion) framework.WaitForMachineDeploymentMachinesToBeUpgraded(ctx, framework.WaitForMachineDeploymentMachinesToBeUpgradedInput{ Lister: mgmtClient, Cluster: input.Cluster, diff --git a/test/e2e/tower_test.go b/test/e2e/tower_test.go index 412e712e..8410dee5 100644 --- a/test/e2e/tower_test.go +++ b/test/e2e/tower_test.go @@ -47,12 +47,15 @@ func init() { func initTowerSession() { var err error - vmService, err = service.NewVMService(goctx.Background(), infrav1.Tower{ - Server: towerServer, - Username: towerUsername, - Password: towerPassword, - AuthMode: towerAuthMode, - SkipTLSVerify: towerSkipTLSVerify}, ctrllog.Log) + vmService, err = service.NewVMService(goctx.Background(), nil, infrav1.Tower{ + TowerClientConfig: infrav1.TowerClientConfig{ + Server: towerServer, + Username: towerUsername, + Password: towerPassword, + AuthMode: towerAuthMode, + SkipTLSVerify: towerSkipTLSVerify, + }, + }, ctrllog.Log) Expect(err).ShouldNot(HaveOccurred()) template, err := vmService.GetVMTemplate(vmTemplate) diff --git a/test/fake/controller_manager_context.go b/test/fake/controller_manager_context.go index cf9beb02..ece4b4e9 100644 --- a/test/fake/controller_manager_context.go +++ b/test/fake/controller_manager_context.go @@ -21,8 +21,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" cgscheme "k8s.io/client-go/kubernetes/scheme" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" diff --git a/test/fake/types.go b/test/fake/types.go index 399ae18a..9139734a 100644 --- a/test/fake/types.go +++ b/test/fake/types.go @@ -25,9 +25,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/storage/names" "k8s.io/utils/ptr" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" - "sigs.k8s.io/cluster-api/util/patch" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/context" diff --git a/test/helpers/cluster.go b/test/helpers/cluster.go index 2d7f2b19..8dd5eb41 100644 --- a/test/helpers/cluster.go +++ b/test/helpers/cluster.go @@ -9,7 +9,7 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" capisecret "sigs.k8s.io/cluster-api/util/secret" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go index fae5d3fc..006b3fc2 100644 --- a/test/helpers/envtest.go +++ b/test/helpers/envtest.go @@ -19,10 +19,13 @@ package helpers import ( goctx "context" "fmt" + "io" + "net" "os" "path" "path/filepath" goruntime "runtime" + "strconv" "strings" "time" @@ -34,6 +37,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" kerrors "k8s.io/apimachinery/pkg/util/errors" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -42,21 +46,28 @@ import ( "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "k8s.io/klog/v2" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" - controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta1" + controlplanev2 "sigs.k8s.io/cluster-api/api/controlplane/kubeadm/v1beta2" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + clusterv2 "sigs.k8s.io/cluster-api/api/core/v1beta2" + kcpwebhooks "sigs.k8s.io/cluster-api/controlplane/kubeadm/webhooks" capiutil "sigs.k8s.io/cluster-api/util" - "sigs.k8s.io/cluster-api/util/kubeconfig" - "sigs.k8s.io/cluster-api/util/patch" + capicontract "sigs.k8s.io/cluster-api/util/contract" + "sigs.k8s.io/cluster-api/util/deprecated/v1beta1/patch" + capiwebhooks "sigs.k8s.io/cluster-api/webhooks" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" ctrlmgr "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" + "sigs.k8s.io/yaml" infrav1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" "github.com/smartxworks/cluster-api-provider-elf/pkg/context" "github.com/smartxworks/cluster-api-provider-elf/pkg/manager" + "github.com/smartxworks/cluster-api-provider-elf/pkg/util/capi/kubeconfig" "github.com/smartxworks/cluster-api-provider-elf/webhooks" ) @@ -83,7 +94,9 @@ func init() { utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) utilruntime.Must(admissionv1.AddToScheme(scheme)) utilruntime.Must(clusterv1.AddToScheme(scheme)) + utilruntime.Must(clusterv2.AddToScheme(scheme)) utilruntime.Must(controlplanev1.AddToScheme(scheme)) + utilruntime.Must(controlplanev2.AddToScheme(scheme)) utilruntime.Must(infrav1.AddToScheme(scheme)) // Get the root of the current file to use in CRD paths. @@ -119,6 +132,7 @@ func NewTestEnvironment(ctx goctx.Context) *TestEnvironment { env := &envtest.Environment{ ErrorIfCRDPathMissing: true, CRDDirectoryPaths: crdPaths, + Scheme: scheme, } if _, err := env.Start(); err != nil { @@ -154,6 +168,39 @@ func NewTestEnvironment(ctx goctx.Context) *TestEnvironment { KubeConfig: env.Config, } managerOpts.AddToManager = func(ctx goctx.Context, ctrlMgrCtx *context.ControllerManagerContext, mgr ctrlmgr.Manager) error { + mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(mgr.GetScheme())) + + apiVersionGetter := func(gk schema.GroupKind) (string, error) { + return getAPIVersionForGroupKind(ctx, mgr.GetClient(), gk) + } + clusterv1.SetAPIVersionGetter(apiVersionGetter) + controlplanev1.SetAPIVersionGetter(apiVersionGetter) + + if err := (&capiwebhooks.Cluster{Client: mgr.GetClient()}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&capiwebhooks.MachineDeployment{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&capiwebhooks.MachineSet{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&capiwebhooks.MachineHealthCheck{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&capiwebhooks.Machine{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&kcpwebhooks.KubeadmControlPlane{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&kcpwebhooks.ScaleValidator{Client: mgr.GetClient()}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&kcpwebhooks.KubeadmControlPlaneTemplate{}).SetupWebhookWithManager(mgr); err != nil { + return err + } + if err := (&webhooks.ElfMachineTemplateValidator{}).SetupWebhookWithManager(mgr); err != nil { return err } @@ -207,6 +254,30 @@ func (t *TestEnvironment) StartManager(ctx goctx.Context) error { return t.Manager.Start(ctx) } +// WaitForWebhooks waits for the webhook server to be available. +func (t *TestEnvironment) WaitForWebhooks(ctx goctx.Context) { + port := t.Env.WebhookInstallOptions.LocalServingPort + + klog.V(2).Infof("Waiting for webhook port %d to be open prior to running tests", port) + timeout := 1 * time.Second + for { + time.Sleep(1 * time.Second) + dialer := &net.Dialer{ + Timeout: timeout, + } + conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + if err != nil { + klog.V(2).Infof("Webhook port is not ready, will retry in %v: %s", timeout, err) + continue + } + if err := conn.Close(); err != nil { + klog.V(2).Infof("Closing connection when testing if webhook port is ready failed: %v", err) + } + klog.V(2).Info("Webhook port is now open. Continuing with tests...") + return + } +} + func (t *TestEnvironment) Stop() error { t.cancel() @@ -347,8 +418,128 @@ func getFilePathToCAPICRDs() []string { pkg := pkgs[0] - return []string{ + paths := []string{ filepath.Join(pkg.Module.Dir, "config", "crd", "bases"), filepath.Join(pkg.Module.Dir, "controlplane", "kubeadm", "config", "crd", "bases"), } + + // CAPI CRDs still serve v1alpha3/v1alpha4 for conversion. Those versions are not available in the + // Go API packages we compile against, which makes the conversion webhook fail decoding requests + // in envtest. For tests, we only need the supported versions, so we strip v1alpha3/v1alpha4. + filtered := make([]string, 0, len(paths)) + for _, p := range paths { + fp, err := filterCRDDirectoryVersions(p, map[string]bool{"v1alpha3": true, "v1alpha4": true}) + if err != nil { + klog.Errorf("Failed to filter CRD dir %s: %v", p, err) + filtered = append(filtered, p) + continue + } + filtered = append(filtered, fp) + } + + return filtered +} + +func filterCRDDirectoryVersions(dir string, dropVersions map[string]bool) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", err + } + tmpDir, err := os.MkdirTemp("", "cape-envtest-crds-") + if err != nil { + return "", err + } + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + srcPath := filepath.Join(dir, name) + dstPath := filepath.Join(tmpDir, name) + + in, err := os.ReadFile(srcPath) //nolint:gosec + if err != nil { + return "", err + } + + out, err := filterSingleCRDYAML(in, dropVersions) + if err != nil { + out = in + } + + if err := os.WriteFile(dstPath, out, 0o600); err != nil { + return "", err + } + } + return tmpDir, nil +} + +func filterSingleCRDYAML(in []byte, dropVersions map[string]bool) ([]byte, error) { + var crd apiextensionsv1.CustomResourceDefinition + if err := yaml.Unmarshal(in, &crd); err != nil { + return nil, err + } + if crd.APIVersion == "" || crd.Kind != "CustomResourceDefinition" || len(crd.Spec.Versions) == 0 { + return nil, io.EOF + } + origLen := len(crd.Spec.Versions) + filtered := crd.Spec.Versions[:0] + for _, v := range crd.Spec.Versions { + if dropVersions[v.Name] { + continue + } + filtered = append(filtered, v) + } + if len(filtered) == 0 || len(filtered) == origLen { + return nil, io.EOF + } + crd.Spec.Versions = filtered + return yaml.Marshal(&crd) +} + +func getAPIVersionForGroupKind(ctx goctx.Context, c client.Reader, gk schema.GroupKind) (string, error) { + if gk.Group == "" { + crdList := &apiextensionsv1.CustomResourceDefinitionList{} + if err := c.List(ctx, crdList); err != nil { + return "", err + } + + var matched *apiextensionsv1.CustomResourceDefinition + for i := range crdList.Items { + crd := &crdList.Items[i] + if crd.Spec.Names.Kind != gk.Kind { + continue + } + if matched != nil && matched.Spec.Group != crd.Spec.Group { + return "", fmt.Errorf("multiple CRDs found for kind %s without group: %s, %s", gk.Kind, matched.Spec.Group, crd.Spec.Group) + } + matched = crd + } + if matched == nil { + return "", fmt.Errorf("crd for kind %s not found", gk.Kind) + } + gk.Group = matched.Spec.Group + } + + crd := &apiextensionsv1.CustomResourceDefinition{} + crdName := capicontract.CalculateCRDName(gk.Group, gk.Kind) + if err := c.Get(ctx, client.ObjectKey{Name: crdName}, crd); err != nil { + return "", err + } + + for _, version := range crd.Spec.Versions { + if version.Storage { + return schema.GroupVersion{Group: gk.Group, Version: version.Name}.String(), nil + } + } + for _, version := range crd.Spec.Versions { + if version.Served { + return schema.GroupVersion{Group: gk.Group, Version: version.Name}.String(), nil + } + } + if len(crd.Spec.Versions) == 0 { + return "", fmt.Errorf("crd %s has no versions", crdName) + } + + return schema.GroupVersion{Group: gk.Group, Version: crd.Spec.Versions[0].Name}.String(), nil } diff --git a/webhooks/elfmachine_webhook_mutation_test.go b/webhooks/elfmachine_webhook_mutation_test.go index 8085e7a2..6958f161 100644 --- a/webhooks/elfmachine_webhook_mutation_test.go +++ b/webhooks/elfmachine_webhook_mutation_test.go @@ -17,7 +17,6 @@ limitations under the License. package webhooks import ( - "context" "encoding/json" "strconv" "testing" @@ -27,7 +26,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -98,7 +97,7 @@ func TestElfMachineMutation(t *testing.T) { mutation := ElfMachineMutation{Client: tc.client} mutation.InjectDecoder(admission.NewDecoder(scheme)) - resp := mutation.Handle(context.Background(), tc.admissionRequest) + resp := mutation.Handle(t.Context(), tc.admissionRequest) g.Expect(resp.Allowed).Should(Equal(tc.expectRespAllowed)) g.Expect(resp.Patches).Should(ContainElements(tc.expectPatchs)) }) diff --git a/webhooks/elfmachine_webhook_validation_test.go b/webhooks/elfmachine_webhook_validation_test.go index 1919547a..e4b49b65 100644 --- a/webhooks/elfmachine_webhook_validation_test.go +++ b/webhooks/elfmachine_webhook_validation_test.go @@ -17,7 +17,6 @@ limitations under the License. package webhooks import ( - goctx "context" "fmt" "testing" @@ -26,7 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" - clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -215,7 +214,7 @@ func TestElfMachineValidatorValidateUpdate(t *testing.T) { validator := &ElfMachineValidator{ Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.Objs...).Build(), } - warnings, err := validator.ValidateUpdate(goctx.Background(), tc.OldEM, tc.EM) + warnings, err := validator.ValidateUpdate(t.Context(), tc.OldEM, tc.EM) g.Expect(warnings).To(BeEmpty()) expectElfMachineTestCase(g, tc, err) }) diff --git a/webhooks/elfmachinetemplate_webhook_mutation_test.go b/webhooks/elfmachinetemplate_webhook_mutation_test.go index 0a057f24..9d323616 100644 --- a/webhooks/elfmachinetemplate_webhook_mutation_test.go +++ b/webhooks/elfmachinetemplate_webhook_mutation_test.go @@ -17,7 +17,6 @@ limitations under the License. package webhooks import ( - "context" "encoding/json" "strconv" "testing" @@ -96,7 +95,7 @@ func TestElfMachineMutationTemplate(t *testing.T) { mutation := ElfMachineTemplateMutation{} mutation.InjectDecoder(admission.NewDecoder(scheme)) - resp := mutation.Handle(context.Background(), tc.admissionRequest) + resp := mutation.Handle(t.Context(), tc.admissionRequest) g.Expect(resp.Allowed).Should(Equal(tc.expectRespAllowed)) g.Expect(resp.Patches).Should(HaveLen(len(tc.expectPatchs))) g.Expect(resp.Patches).Should(ContainElements(tc.expectPatchs)) diff --git a/webhooks/elfmachinetemplate_webhook_validation_test.go b/webhooks/elfmachinetemplate_webhook_validation_test.go index cf04d81d..55426216 100644 --- a/webhooks/elfmachinetemplate_webhook_validation_test.go +++ b/webhooks/elfmachinetemplate_webhook_validation_test.go @@ -17,7 +17,6 @@ limitations under the License. package webhooks import ( - goctx "context" "testing" . "github.com/onsi/gomega" @@ -124,7 +123,7 @@ func TestElfMachineTemplateValidatorValidateCreate(t *testing.T) { for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - warnings, err := validator.ValidateCreate(goctx.Background(), tc.EMT) + warnings, err := validator.ValidateCreate(t.Context(), tc.EMT) g.Expect(warnings).To(BeEmpty()) expectTestCase(g, tc, err) }) @@ -250,7 +249,7 @@ func TestElfMachineTemplateValidatorValidateUpdate(t *testing.T) { for _, tc := range tests { t.Run(tc.Name, func(t *testing.T) { - warnings, err := validator.ValidateUpdate(goctx.Background(), tc.OldEMT, tc.EMT) + warnings, err := validator.ValidateUpdate(t.Context(), tc.OldEMT, tc.EMT) g.Expect(warnings).To(BeEmpty()) expectTestCase(g, tc, err) })