From 3fe374ad2fb8b62f807ee0aa35a7be18220064f0 Mon Sep 17 00:00:00 2001 From: rabi Date: Thu, 4 Apr 2024 10:11:12 +0530 Subject: [PATCH] Add initial functional tests Signed-off-by: rabi --- Makefile | 6 +- go.mod | 4 +- go.sum | 4 + tests/functional/base_test.go | 150 +++ .../metal3-crds/metal3.io_baremetalhosts.yaml | 1145 +++++++++++++++++ .../openstackbaremetalset_controller_test.go | 200 +++ tests/functional/suit_test.go | 183 +++ 7 files changed, 1687 insertions(+), 5 deletions(-) create mode 100644 tests/functional/base_test.go create mode 100644 tests/functional/metal3-crds/metal3.io_baremetalhosts.yaml create mode 100644 tests/functional/openstackbaremetalset_controller_test.go create mode 100644 tests/functional/suit_test.go diff --git a/Makefile b/Makefile index c5b3dd0..6f00be6 100644 --- a/Makefile +++ b/Makefile @@ -128,7 +128,7 @@ vet: gowork ## Run go vet against code. .PHONY: test test: manifests generate fmt vet envtest ginkgo ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) --coverprofile cover.out --cover --covermode=atomic --coverpkg=../../pkg/...,../../controllers,../../api/v1beta1 --trace ./... + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) --trace --cover --coverpkg=../../pkg/...,../../controllers,../../api/v1beta1 --coverprofile cover.out --covermode=atomic ${PROC_CMD} $(GINKGO_ARGS) ./tests/... ##@ Build @@ -337,9 +337,7 @@ govet: get-ci-tools GOWORK=off $(CI_TOOLS_REPO_DIR)/test-runner/govet.sh ./api # Run go test against code -gotest: get-ci-tools - GOWORK=off $(CI_TOOLS_REPO_DIR)/test-runner/gotest.sh - GOWORK=off $(CI_TOOLS_REPO_DIR)/test-runner/gotest.sh ./api +gotest: test .PHONY: golangci-lint golangci-lint: diff --git a/go.mod b/go.mod index 4a6f60b..4f6993d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/go-logr/logr v1.4.1 github.com/golang/glog v1.2.0 + github.com/google/uuid v1.6.0 github.com/metal3-io/baremetal-operator/apis v0.5.1 github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 @@ -38,17 +39,18 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.12 // 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/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.4.0 // 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/openshift/api v3.9.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect diff --git a/go.sum b/go.sum index 45340ad..695c41e 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 h1:VzM3TYHDgqPkettiP6I6q2jOeQFL4nrJM+UcAc4f6Fs= +github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0/go.mod h1:nqCI7aelBJU61wiBeeZWJ6oi4bJy5nrjkM6lWIMA4j0= 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -94,6 +96,8 @@ github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8 github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/openshift/api v3.9.0+incompatible h1:fJ/KsefYuZAjmrr3+5U9yZIZbTOpVkDDLDLFresAeYs= +github.com/openshift/api v3.9.0+incompatible/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240412091425-bb628ded5eb8 h1:5ywsORdn4y7vNO732ODwYlmfRXnEphfoqB6PCYWt9r8= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240412091425-bb628ded5eb8/go.mod h1:gqByVGUdKQB/NkhKV4eD+8NWYkHq961nC96rTCB3ywE= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go new file mode 100644 index 0000000..577540f --- /dev/null +++ b/tests/functional/base_test.go @@ -0,0 +1,150 @@ +/* +Copyright 2023. + +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 functional + +import ( + "strings" + + metal3v1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + . "github.com/onsi/gomega" //revive:disable:dot-imports + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +// Create OpenstackBaremetalSet in k8s and test that no errors occur +func CreateBaremetalSet(name types.NamespacedName, spec map[string]interface{}) *unstructured.Unstructured { + instance := DefaultBaremetalSetTemplate(name, spec) + return th.CreateUnstructured(instance) +} + +// Build OpenStackBaremetalSet struct and fill it with preset values +func DefaultBaremetalSetTemplate(name types.NamespacedName, spec map[string]interface{}) map[string]interface{} { + return map[string]interface{}{ + + "apiVersion": "baremetal.openstack.org/v1beta1", + "kind": "OpenStackBaremetalSet", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } +} + +// Build BaremetalSetSpec struct and fill it with preset values +func DefaultBaremetalSetSpec(name types.NamespacedName, withProvInterface bool) map[string]interface{} { + spec := map[string]interface{}{ + "baremetalHosts": map[string]interface{}{ + "compute-0": map[string]interface{}{ + "ctlPlaneIP": "10.0.0.1", + }, + }, + "deploymentSSHSecret": "mysecret", + "ctlplaneInterface": "eth0", + "bmhNamespace": name.Namespace, + } + if withProvInterface { + spec["provisioningInterface"] = "eth1" + spec["osContainerImageUrl"] = "quay.io/podified-antelope-centos9/edpm-hardened-uefi@latest" + spec["agentImageUrl"] = "quay.io/openstack-k8s-operators/openstack-baremetal-operator-agent@latest" + spec["apacheImageUrl"] = "registry.redhat.io/rhel8/httpd-24@latest" + spec["osImage"] = "edpm-hardened-uefi.qcow2" + } + + return spec + +} + +// Default BMH Template with preset values +func DefaultBMHTemplate(name types.NamespacedName) map[string]interface{} { + return map[string]interface{}{ + "apiVersion": "metal3.io/v1alpha1", + "kind": "BareMetalHost", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + "annotations": map[string]interface{}{ + "inspect.metal3.io": "disabled", + }, + }, + "spec": map[string]interface{}{ + "bmc": map[string]interface{}{ + "address": "fake_address", + "credentialsName": "fake_credential", + }, + "bootMACAddress": "52:54:00:39:a7:44", + "bootMode": "UEFI", + "online": false, + }, + } +} + +// Get BaremetalSet +func GetBaremetalSet(name types.NamespacedName) *baremetalv1.OpenStackBaremetalSet { + instance := &baremetalv1.OpenStackBaremetalSet{} + Eventually(func(g Gomega) error { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + return nil + }, timeout, interval).Should(Succeed()) + return instance +} + +// Create BaremetalHost +func CreateBaremetalHost(name types.NamespacedName) *unstructured.Unstructured { + instance := DefaultBMHTemplate(name) + return th.CreateUnstructured(instance) +} + +// Get BaremetalHost +func GetBaremetalHost(name types.NamespacedName) *metal3v1.BareMetalHost { + instance := &metal3v1.BareMetalHost{} + Eventually(func(g Gomega) error { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + return nil + }, timeout, interval).Should(Succeed()) + return instance +} + +// Get BaremetalSet conditions +func BaremetalSetConditionGetter(name types.NamespacedName) condition.Conditions { + instance := GetBaremetalSet(name) + return instance.Status.Conditions +} + +// Create DeploymentSSHSecret +func CreateSSHSecret(name types.NamespacedName) *corev1.Secret { + return th.CreateSecret( + types.NamespacedName{Namespace: name.Namespace, Name: name.Name}, + map[string][]byte{ + "ssh-privatekey": []byte("blah"), + "authorized_keys": []byte("blih"), + }, + ) +} + +// Get ProvisionServer +func GetProvisionServer(name types.NamespacedName) *baremetalv1.OpenStackProvisionServer { + instance := &baremetalv1.OpenStackProvisionServer{} + name.Name = strings.Join([]string{name.Name, "provisionserver"}, "-") + Eventually(func(g Gomega) error { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + return nil + }, timeout, interval).Should(Succeed()) + return instance +} diff --git a/tests/functional/metal3-crds/metal3.io_baremetalhosts.yaml b/tests/functional/metal3-crds/metal3.io_baremetalhosts.yaml new file mode 100644 index 0000000..007f1e7 --- /dev/null +++ b/tests/functional/metal3-crds/metal3.io_baremetalhosts.yaml @@ -0,0 +1,1145 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.1 + name: baremetalhosts.metal3.io +spec: + group: metal3.io + names: + kind: BareMetalHost + listKind: BareMetalHostList + plural: baremetalhosts + shortNames: + - bmh + - bmhost + singular: baremetalhost + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Operational status + jsonPath: .status.operationalStatus + name: Status + priority: 1 + type: string + - description: Provisioning status + jsonPath: .status.provisioning.state + name: State + type: string + - description: Consumer using this host + jsonPath: .spec.consumerRef.name + name: Consumer + type: string + - description: Address of management controller + jsonPath: .spec.bmc.address + name: BMC + priority: 1 + type: string + - description: The type of hardware detected + jsonPath: .status.hardwareProfile + name: Hardware_Profile + priority: 1 + type: string + - description: Whether the host is online or not + jsonPath: .spec.online + name: Online + type: string + - description: Type of the most recent error + jsonPath: .status.errorType + name: Error + type: string + - description: Time duration since creation of BaremetalHost + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BareMetalHost is the Schema for the baremetalhosts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BareMetalHostSpec defines the desired state of BareMetalHost. + properties: + architecture: + description: CPU architecture of the host, e.g. "x86_64" or "aarch64". + If unset, eventually populated by inspection. + type: string + automatedCleaningMode: + default: metadata + description: When set to disabled, automated cleaning will be avoided + during provisioning and deprovisioning. + enum: + - metadata + - disabled + type: string + bmc: + description: How do we connect to the BMC? + properties: + address: + description: Address holds the URL for accessing the controller + on the network. + type: string + credentialsName: + description: The name of the secret containing the BMC credentials + (requires keys "username" and "password"). + type: string + disableCertificateVerification: + description: DisableCertificateVerification disables verification + of server certificates when using HTTPS to connect to the BMC. + This is required when the server certificate is self-signed, + but is insecure because it allows a man-in-the-middle to intercept + the connection. + type: boolean + required: + - address + - credentialsName + type: object + bootMACAddress: + description: Which MAC address will PXE boot? This is optional for + some types, but required for libvirt VMs driven by vbmc. + pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' + type: string + bootMode: + description: Select the method of initializing the hardware during + boot. Defaults to UEFI. + enum: + - UEFI + - UEFISecureBoot + - legacy + type: string + consumerRef: + description: ConsumerRef can be used to store information about something + that is using a host. When it is not empty, the host is considered + "in use". + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic + customDeploy: + description: A custom deploy procedure. + properties: + method: + description: Custom deploy method name. This name is specific + to the deploy ramdisk used. If you don't have a custom deploy + ramdisk, you shouldn't use CustomDeploy. + type: string + required: + - method + type: object + description: + description: Description is a human-entered text used to help identify + the host + type: string + externallyProvisioned: + description: ExternallyProvisioned means something else is managing + the image running on the host and the operator should only manage + the power status and hardware inventory inspection. If the Image + field is filled in, this field is ignored. + type: boolean + firmware: + description: BIOS configuration for bare metal server + properties: + simultaneousMultithreadingEnabled: + description: 'Allows a single physical processor core to appear + as several logical processors. This supports following options: + true, false.' + enum: + - true + - false + type: boolean + sriovEnabled: + description: 'SR-IOV support enables a hypervisor to create virtual + instances of a PCI-express device, potentially increasing performance. + This supports following options: true, false.' + enum: + - true + - false + type: boolean + virtualizationEnabled: + description: 'Supports the virtualization of platform hardware. + This supports following options: true, false.' + enum: + - true + - false + type: boolean + type: object + hardwareProfile: + description: What is the name of the hardware profile for this host? + Hardware profiles are deprecated and should not be used. Use the + separate fields Architecture and RootDeviceHints instead. Set to + "empty" to prepare for the future version of the API without hardware + profiles. + type: string + image: + description: Image holds the details of the image to be provisioned. + properties: + checksum: + description: Checksum is the checksum for the image. + type: string + checksumType: + description: ChecksumType is the checksum algorithm for the image, + e.g md5, sha256 or sha512. The special value "auto" can be used + to detect the algorithm from the checksum. If missing, MD5 is + used. If in doubt, use "auto". + enum: + - md5 + - sha256 + - sha512 + - auto + type: string + format: + description: DiskFormat contains the format of the image (raw, + qcow2, ...). Needs to be set to raw for raw images streaming. + Note live-iso means an iso referenced by the url will be live-booted + and not deployed to disk, and in this case the checksum options + are not required and if specified will be ignored. + enum: + - raw + - qcow2 + - vdi + - vmdk + - live-iso + type: string + url: + description: URL is a location of an image to deploy. + type: string + required: + - url + type: object + metaData: + description: MetaData holds the reference to the Secret containing + host metadata (e.g. meta_data.json) which is passed to the Config + Drive. + 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 + networkData: + description: NetworkData holds the reference to the Secret containing + network configuration (e.g content of network_data.json) which is + passed to the Config Drive. + 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 + online: + description: Should the server be online? + type: boolean + preprovisioningNetworkDataName: + description: PreprovisioningNetworkDataName is the name of the Secret + in the local namespace containing network configuration (e.g content + of network_data.json) which is passed to the preprovisioning image, + and to the Config Drive if not overridden by specifying NetworkData. + type: string + raid: + description: RAID configuration for bare metal server + properties: + hardwareRAIDVolumes: + description: The list of logical disks for hardware RAID, if rootDeviceHints + isn't used, first volume is root volume. You can set the value + of this field to `[]` to clear all the hardware RAID configurations. + items: + description: HardwareRAIDVolume defines the desired configuration + of volume in hardware RAID. + properties: + controller: + description: The name of the RAID controller to use + type: string + level: + description: 'RAID level for the logical disk. The following + levels are supported: 0;1;2;5;6;1+0;5+0;6+0.' + enum: + - "0" + - "1" + - "2" + - "5" + - "6" + - 1+0 + - 5+0 + - 6+0 + type: string + name: + description: Name of the volume. Should be unique within + the Node. If not specified, volume name will be auto-generated. + maxLength: 64 + type: string + numberOfPhysicalDisks: + description: Integer, number of physical disks to use for + the logical disk. Defaults to minimum number of disks + required for the particular RAID level. + minimum: 1 + type: integer + physicalDisks: + description: Optional list of physical disk names to be + used for the Hardware RAID volumes. The disk names are + interpreted by the Hardware RAID controller, and the format + is hardware specific. + items: + type: string + type: array + rotational: + description: Select disks with only rotational or solid-state + storage + type: boolean + sizeGibibytes: + description: Size (Integer) of the logical disk to be created + in GiB. If unspecified or set be 0, the maximum capacity + of disk will be used for logical disk. + minimum: 0 + type: integer + required: + - level + type: object + nullable: true + type: array + softwareRAIDVolumes: + description: The list of logical disks for software RAID, if rootDeviceHints + isn't used, first volume is root volume. If HardwareRAIDVolumes + is set this item will be invalid. The number of created Software + RAID devices must be 1 or 2. If there is only one Software RAID + device, it has to be a RAID-1. If there are two, the first one + has to be a RAID-1, while the RAID level for the second one + can be 0, 1, or 1+0. As the first RAID device will be the deployment + device, enforcing a RAID-1 reduces the risk of ending up with + a non-booting node in case of a disk failure. Software RAID + will always be deleted. + items: + description: SoftwareRAIDVolume defines the desired configuration + of volume in software RAID. + properties: + level: + description: 'RAID level for the logical disk. The following + levels are supported: 0;1;1+0.' + enum: + - "0" + - "1" + - 1+0 + type: string + physicalDisks: + description: A list of device hints, the number of items + should be greater than or equal to 2. + items: + description: RootDeviceHints holds the hints for specifying + the storage location for the root filesystem for the + image. + properties: + deviceName: + description: A Linux device name like "/dev/vda", + or a by-path link to it like "/dev/disk/by-path/pci-0000:01:00.0-scsi-0:2:0:0". + The hint must match the actual value exactly. + type: string + hctl: + description: A SCSI bus address like 0:0:0:0. The + hint must match the actual value exactly. + type: string + minSizeGigabytes: + description: The minimum size of the device in Gigabytes. + minimum: 0 + type: integer + model: + description: A vendor-specific device identifier. + The hint can be a substring of the actual value. + type: string + rotational: + description: True if the device should use spinning + media, false otherwise. + type: boolean + serialNumber: + description: Device serial number. The hint must match + the actual value exactly. + type: string + vendor: + description: The name of the vendor or manufacturer + of the device. The hint can be a substring of the + actual value. + type: string + wwn: + description: Unique storage identifier. The hint must + match the actual value exactly. + type: string + wwnVendorExtension: + description: Unique vendor storage identifier. The + hint must match the actual value exactly. + type: string + wwnWithExtension: + description: Unique storage identifier with the vendor + extension appended. The hint must match the actual + value exactly. + type: string + type: object + minItems: 2 + type: array + sizeGibibytes: + description: Size (Integer) of the logical disk to be created + in GiB. If unspecified or set be 0, the maximum capacity + of disk will be used for logical disk. + minimum: 0 + type: integer + required: + - level + type: object + maxItems: 2 + nullable: true + type: array + type: object + rootDeviceHints: + description: Provide guidance about how to choose the device for the + image being provisioned. + properties: + deviceName: + description: A Linux device name like "/dev/vda", or a by-path + link to it like "/dev/disk/by-path/pci-0000:01:00.0-scsi-0:2:0:0". + The hint must match the actual value exactly. + type: string + hctl: + description: A SCSI bus address like 0:0:0:0. The hint must match + the actual value exactly. + type: string + minSizeGigabytes: + description: The minimum size of the device in Gigabytes. + minimum: 0 + type: integer + model: + description: A vendor-specific device identifier. The hint can + be a substring of the actual value. + type: string + rotational: + description: True if the device should use spinning media, false + otherwise. + type: boolean + serialNumber: + description: Device serial number. The hint must match the actual + value exactly. + type: string + vendor: + description: The name of the vendor or manufacturer of the device. + The hint can be a substring of the actual value. + type: string + wwn: + description: Unique storage identifier. The hint must match the + actual value exactly. + type: string + wwnVendorExtension: + description: Unique vendor storage identifier. The hint must match + the actual value exactly. + type: string + wwnWithExtension: + description: Unique storage identifier with the vendor extension + appended. The hint must match the actual value exactly. + type: string + type: object + taints: + description: Taints is the full, authoritative list of taints to apply + to the corresponding Machine. This list will overwrite any modifications + made to the Machine on an ongoing basis. + items: + description: The node this Taint is attached to has the "effect" + on any pod that does not tolerate the Taint. + properties: + effect: + description: Required. The effect of the taint on pods that + do not tolerate the taint. Valid effects are NoSchedule, PreferNoSchedule + and NoExecute. + type: string + key: + description: Required. The taint key to be applied to a node. + type: string + timeAdded: + description: TimeAdded represents the time at which the taint + was added. It is only written for NoExecute taints. + format: date-time + type: string + value: + description: The taint value corresponding to the taint key. + type: string + required: + - effect + - key + type: object + type: array + userData: + description: UserData holds the reference to the Secret containing + the user data to be passed to the host before it boots. + 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 + required: + - online + type: object + status: + description: BareMetalHostStatus defines the observed state of BareMetalHost. + properties: + errorCount: + default: 0 + description: ErrorCount records how many times the host has encoutered + an error since the last successful operation + type: integer + errorMessage: + description: the last error message reported by the provisioning subsystem + type: string + errorType: + description: ErrorType indicates the type of failure encountered when + the OperationalStatus is OperationalStatusError + enum: + - provisioned registration error + - registration error + - inspection error + - preparation error + - provisioning error + - power management error + type: string + goodCredentials: + description: the last credentials we were able to validate as working + properties: + credentials: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + 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 + credentialsVersion: + type: string + type: object + hardware: + description: The hardware discovered to exist on the host. + properties: + cpu: + description: CPU describes one processor on the host. + properties: + arch: + type: string + clockMegahertz: + description: ClockSpeed is a clock speed in MHz + format: double + type: number + count: + type: integer + flags: + items: + type: string + type: array + model: + type: string + type: object + firmware: + description: Firmware describes the firmware on the host. + properties: + bios: + description: The BIOS for this firmware + properties: + date: + description: The release/build date for this BIOS + type: string + vendor: + description: The vendor name for this BIOS + type: string + version: + description: The version of the BIOS + type: string + type: object + type: object + hostname: + type: string + nics: + items: + description: NIC describes one network interface on the host. + properties: + ip: + description: The IP address of the interface. This will + be an IPv4 or IPv6 address if one is present. If both + IPv4 and IPv6 addresses are present in a dual-stack environment, + two nics will be output, one with each IP. + type: string + mac: + description: The device MAC address + pattern: '[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}' + type: string + model: + description: The vendor and product IDs of the NIC, e.g. + "0x8086 0x1572" + type: string + name: + description: The name of the network interface, e.g. "en0" + type: string + pxe: + description: Whether the NIC is PXE Bootable + type: boolean + speedGbps: + description: The speed of the device in Gigabits per second + type: integer + vlanId: + description: The untagged VLAN ID + format: int32 + maximum: 4094 + minimum: 0 + type: integer + vlans: + description: The VLANs available + items: + description: VLAN represents the name and ID of a VLAN. + properties: + id: + description: VLANID is a 12-bit 802.1Q VLAN identifier + format: int32 + maximum: 4094 + minimum: 0 + type: integer + name: + type: string + type: object + type: array + type: object + type: array + ramMebibytes: + type: integer + storage: + items: + description: Storage describes one storage device (disk, SSD, + etc.) on the host. + properties: + alternateNames: + description: A list of alternate Linux device names of the + disk, e.g. "/dev/sda". Note that this list is not exhaustive, + and names may not be stable across reboots. + items: + type: string + type: array + hctl: + description: The SCSI location of the device + type: string + model: + description: Hardware model + type: string + name: + description: A Linux device name of the disk, e.g. "/dev/disk/by-path/pci-0000:01:00.0-scsi-0:2:0:0". + This will be a name that is stable across reboots if one + is available. + type: string + rotational: + description: Whether this disk represents rotational storage. + This field is not recommended for usage, please prefer + using 'Type' field instead, this field will be deprecated + eventually. + type: boolean + serialNumber: + description: The serial number of the device + type: string + sizeBytes: + description: The size of the disk in Bytes + format: int64 + type: integer + type: + description: 'Device type, one of: HDD, SSD, NVME.' + enum: + - HDD + - SSD + - NVME + type: string + vendor: + description: The name of the vendor of the device + type: string + wwn: + description: The WWN of the device + type: string + wwnVendorExtension: + description: The WWN Vendor extension of the device + type: string + wwnWithExtension: + description: The WWN with the extension + type: string + type: object + type: array + systemVendor: + description: HardwareSystemVendor stores details about the whole + hardware system. + properties: + manufacturer: + type: string + productName: + type: string + serialNumber: + type: string + type: object + type: object + hardwareProfile: + description: The name of the profile matching the hardware details. + type: string + lastUpdated: + description: LastUpdated identifies when this status was last observed. + format: date-time + type: string + operationHistory: + description: OperationHistory holds information about operations performed + on this host. + properties: + deprovision: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + inspect: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + provision: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + register: + description: OperationMetric contains metadata about an operation + (inspection, provisioning, etc.) used for tracking metrics. + properties: + end: + format: date-time + nullable: true + type: string + start: + format: date-time + nullable: true + type: string + type: object + type: object + operationalStatus: + description: OperationalStatus holds the status of the host + enum: + - "" + - OK + - discovered + - error + - delayed + - detached + type: string + poweredOn: + description: indicator for whether or not the host is powered on + type: boolean + provisioning: + description: Information tracked by the provisioner. + properties: + ID: + description: The machine's UUID from the underlying provisioning + tool + type: string + bootMode: + description: BootMode indicates the boot mode used to provision + the node + enum: + - UEFI + - UEFISecureBoot + - legacy + type: string + customDeploy: + description: Custom deploy procedure applied to the host. + properties: + method: + description: Custom deploy method name. This name is specific + to the deploy ramdisk used. If you don't have a custom deploy + ramdisk, you shouldn't use CustomDeploy. + type: string + required: + - method + type: object + firmware: + description: The Bios set by the user + properties: + simultaneousMultithreadingEnabled: + description: 'Allows a single physical processor core to appear + as several logical processors. This supports following options: + true, false.' + enum: + - true + - false + type: boolean + sriovEnabled: + description: 'SR-IOV support enables a hypervisor to create + virtual instances of a PCI-express device, potentially increasing + performance. This supports following options: true, false.' + enum: + - true + - false + type: boolean + virtualizationEnabled: + description: 'Supports the virtualization of platform hardware. + This supports following options: true, false.' + enum: + - true + - false + type: boolean + type: object + image: + description: Image holds the details of the last image successfully + provisioned to the host. + properties: + checksum: + description: Checksum is the checksum for the image. + type: string + checksumType: + description: ChecksumType is the checksum algorithm for the + image, e.g md5, sha256 or sha512. The special value "auto" + can be used to detect the algorithm from the checksum. If + missing, MD5 is used. If in doubt, use "auto". + enum: + - md5 + - sha256 + - sha512 + - auto + type: string + format: + description: DiskFormat contains the format of the image (raw, + qcow2, ...). Needs to be set to raw for raw images streaming. + Note live-iso means an iso referenced by the url will be + live-booted and not deployed to disk, and in this case the + checksum options are not required and if specified will + be ignored. + enum: + - raw + - qcow2 + - vdi + - vmdk + - live-iso + type: string + url: + description: URL is a location of an image to deploy. + type: string + required: + - url + type: object + raid: + description: The Raid set by the user + properties: + hardwareRAIDVolumes: + description: The list of logical disks for hardware RAID, + if rootDeviceHints isn't used, first volume is root volume. + You can set the value of this field to `[]` to clear all + the hardware RAID configurations. + items: + description: HardwareRAIDVolume defines the desired configuration + of volume in hardware RAID. + properties: + controller: + description: The name of the RAID controller to use + type: string + level: + description: 'RAID level for the logical disk. The following + levels are supported: 0;1;2;5;6;1+0;5+0;6+0.' + enum: + - "0" + - "1" + - "2" + - "5" + - "6" + - 1+0 + - 5+0 + - 6+0 + type: string + name: + description: Name of the volume. Should be unique within + the Node. If not specified, volume name will be auto-generated. + maxLength: 64 + type: string + numberOfPhysicalDisks: + description: Integer, number of physical disks to use + for the logical disk. Defaults to minimum number of + disks required for the particular RAID level. + minimum: 1 + type: integer + physicalDisks: + description: Optional list of physical disk names to + be used for the Hardware RAID volumes. The disk names + are interpreted by the Hardware RAID controller, and + the format is hardware specific. + items: + type: string + type: array + rotational: + description: Select disks with only rotational or solid-state + storage + type: boolean + sizeGibibytes: + description: Size (Integer) of the logical disk to be + created in GiB. If unspecified or set be 0, the maximum + capacity of disk will be used for logical disk. + minimum: 0 + type: integer + required: + - level + type: object + nullable: true + type: array + softwareRAIDVolumes: + description: The list of logical disks for software RAID, + if rootDeviceHints isn't used, first volume is root volume. + If HardwareRAIDVolumes is set this item will be invalid. + The number of created Software RAID devices must be 1 or + 2. If there is only one Software RAID device, it has to + be a RAID-1. If there are two, the first one has to be a + RAID-1, while the RAID level for the second one can be 0, + 1, or 1+0. As the first RAID device will be the deployment + device, enforcing a RAID-1 reduces the risk of ending up + with a non-booting node in case of a disk failure. Software + RAID will always be deleted. + items: + description: SoftwareRAIDVolume defines the desired configuration + of volume in software RAID. + properties: + level: + description: 'RAID level for the logical disk. The following + levels are supported: 0;1;1+0.' + enum: + - "0" + - "1" + - 1+0 + type: string + physicalDisks: + description: A list of device hints, the number of items + should be greater than or equal to 2. + items: + description: RootDeviceHints holds the hints for specifying + the storage location for the root filesystem for + the image. + properties: + deviceName: + description: A Linux device name like "/dev/vda", + or a by-path link to it like "/dev/disk/by-path/pci-0000:01:00.0-scsi-0:2:0:0". + The hint must match the actual value exactly. + type: string + hctl: + description: A SCSI bus address like 0:0:0:0. + The hint must match the actual value exactly. + type: string + minSizeGigabytes: + description: The minimum size of the device in + Gigabytes. + minimum: 0 + type: integer + model: + description: A vendor-specific device identifier. + The hint can be a substring of the actual value. + type: string + rotational: + description: True if the device should use spinning + media, false otherwise. + type: boolean + serialNumber: + description: Device serial number. The hint must + match the actual value exactly. + type: string + vendor: + description: The name of the vendor or manufacturer + of the device. The hint can be a substring of + the actual value. + type: string + wwn: + description: Unique storage identifier. The hint + must match the actual value exactly. + type: string + wwnVendorExtension: + description: Unique vendor storage identifier. + The hint must match the actual value exactly. + type: string + wwnWithExtension: + description: Unique storage identifier with the + vendor extension appended. The hint must match + the actual value exactly. + type: string + type: object + minItems: 2 + type: array + sizeGibibytes: + description: Size (Integer) of the logical disk to be + created in GiB. If unspecified or set be 0, the maximum + capacity of disk will be used for logical disk. + minimum: 0 + type: integer + required: + - level + type: object + maxItems: 2 + nullable: true + type: array + type: object + rootDeviceHints: + description: The RootDevicehints set by the user + properties: + deviceName: + description: A Linux device name like "/dev/vda", or a by-path + link to it like "/dev/disk/by-path/pci-0000:01:00.0-scsi-0:2:0:0". + The hint must match the actual value exactly. + type: string + hctl: + description: A SCSI bus address like 0:0:0:0. The hint must + match the actual value exactly. + type: string + minSizeGigabytes: + description: The minimum size of the device in Gigabytes. + minimum: 0 + type: integer + model: + description: A vendor-specific device identifier. The hint + can be a substring of the actual value. + type: string + rotational: + description: True if the device should use spinning media, + false otherwise. + type: boolean + serialNumber: + description: Device serial number. The hint must match the + actual value exactly. + type: string + vendor: + description: The name of the vendor or manufacturer of the + device. The hint can be a substring of the actual value. + type: string + wwn: + description: Unique storage identifier. The hint must match + the actual value exactly. + type: string + wwnVendorExtension: + description: Unique vendor storage identifier. The hint must + match the actual value exactly. + type: string + wwnWithExtension: + description: Unique storage identifier with the vendor extension + appended. The hint must match the actual value exactly. + type: string + type: object + state: + description: An indiciator for what the provisioner is doing with + the host. + type: string + required: + - ID + - state + type: object + triedCredentials: + description: the last credentials we sent to the provisioning backend + properties: + credentials: + description: SecretReference represents a Secret Reference. It + has enough information to retrieve secret in any namespace + 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 + credentialsVersion: + type: string + type: object + required: + - errorCount + - errorMessage + - hardwareProfile + - operationalStatus + - poweredOn + - provisioning + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/tests/functional/openstackbaremetalset_controller_test.go b/tests/functional/openstackbaremetalset_controller_test.go new file mode 100644 index 0000000..6f66115 --- /dev/null +++ b/tests/functional/openstackbaremetalset_controller_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2023. + +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 functional + +import ( + metal3v1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1" + + //revive:disable-next-line:dot-imports + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("BaremetalSet Test", func() { + var baremetalSetName types.NamespacedName + var bmhName types.NamespacedName + var deploymentSecretName types.NamespacedName + + BeforeEach(func() { + baremetalSetName = types.NamespacedName{ + Name: "edpm-compute-baremetalset", + Namespace: namespace, + } + bmhName = types.NamespacedName{ + Name: "compute-0", + Namespace: namespace, + } + deploymentSecretName = types.NamespacedName{ + Name: "mysecret", + Namespace: namespace, + } + }) + + When("A BaremetalSet resource created", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName)) + bmh := GetBaremetalHost(bmhName) + Eventually(func(g Gomega) { + // OpenStackBaremetalSet has the same name as OpenStackDataPlaneNodeSet + bmh.Status.Provisioning.State = metal3v1.StateAvailable + g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed()) + + }, th.Timeout, th.Interval).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, DefaultBaremetalSetSpec(bmhName, false))) + }) + It("should have the Spec fields initialized", func() { + baremetalSetInstance := GetBaremetalSet(baremetalSetName) + spec := baremetalv1.OpenStackBaremetalSetSpec{ + BaremetalHosts: map[string]baremetalv1.InstanceSpec{ + "compute-0": { + CtlPlaneIP: "10.0.0.1", + UserData: nil, + NetworkData: nil, + PreprovisioningNetworkDataName: "", + }, + }, + OSImage: "", + OSContainerImageURL: "", + ApacheImageURL: "", + AgentImageURL: "", + UserData: nil, + NetworkData: nil, + AutomatedCleaningMode: "metadata", + ProvisionServerName: "", + ProvisioningInterface: "", + DeploymentSSHSecret: "mysecret", + CtlplaneInterface: "eth0", + CtlplaneGateway: "", + CtlplaneNetmask: "255.255.255.0", + BmhNamespace: baremetalSetName.Namespace, + BmhLabelSelector: nil, + HardwareReqs: baremetalv1.HardwareReqs{ + CPUReqs: baremetalv1.CPUReqs{ + Arch: "", + CountReq: baremetalv1.CPUCountReq{Count: 0, ExactMatch: false}, + MhzReq: baremetalv1.CPUMhzReq{Mhz: 0, ExactMatch: false}, + }, + MemReqs: baremetalv1.MemReqs{ + GbReq: baremetalv1.MemGbReq{Gb: 0, ExactMatch: false}, + }, + DiskReqs: baremetalv1.DiskReqs{ + GbReq: baremetalv1.DiskGbReq{Gb: 0, ExactMatch: false}, + SSDReq: baremetalv1.DiskSSDReq{SSD: false, ExactMatch: false}, + }, + }, + PasswordSecret: nil, + CloudUserName: "cloud-admin", + DomainName: "", + BootstrapDNS: nil, + DNSSearchDomains: nil, + } + Expect(baremetalSetInstance.Spec).Should(Equal(spec)) + }) + It("should have Conditions initialized", func() { + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + condition.InputReadyCondition, + corev1.ConditionFalse, + ) + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + baremetalv1.OpenStackBaremetalSetProvServerReadyCondition, + corev1.ConditionUnknown, + ) + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + baremetalv1.OpenStackBaremetalSetBmhProvisioningReadyCondition, + corev1.ConditionUnknown, + ) + }) + }) + + When("A deployment ssh secret is created", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName)) + bmh := GetBaremetalHost(bmhName) + Eventually(func(g Gomega) { + bmh.Status.Provisioning.State = metal3v1.StateAvailable + g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed()) + + }, th.Timeout, th.Interval).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateSSHSecret(deploymentSecretName)) + DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, DefaultBaremetalSetSpec(bmhName, false))) + }) + It("Should set Input Ready", func() { + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + + }) + }) + + When("Provisioning interface provided", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName)) + bmh := GetBaremetalHost(bmhName) + Eventually(func(g Gomega) { + bmh.Status.Provisioning.State = metal3v1.StateAvailable + g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed()) + + }, th.Timeout, th.Interval).Should(Succeed()) + DeferCleanup(th.DeleteInstance, CreateSSHSecret(deploymentSecretName)) + DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, DefaultBaremetalSetSpec(bmhName, true))) + }) + It("Prov Server should have the Spec fields initialized", func() { + provServer := GetProvisionServer(baremetalSetName) + spec := baremetalv1.OpenStackProvisionServerSpec{ + Port: 6190, + Interface: "eth1", + OSImage: "edpm-hardened-uefi.qcow2", + OSContainerImageURL: "quay.io/podified-antelope-centos9/edpm-hardened-uefi@latest", + ApacheImageURL: "registry.redhat.io/rhel8/httpd-24@latest", + AgentImageURL: "quay.io/openstack-k8s-operators/openstack-baremetal-operator-agent@latest", + NodeSelector: nil, + Resources: corev1.ResourceRequirements{Limits: nil, Requests: nil, Claims: nil}, + } + Expect(provServer.Spec).Should(Equal(spec)) + }) + + It("Should set Provision Server Ready", func() { + th.ExpectCondition( + baremetalSetName, + ConditionGetterFunc(BaremetalSetConditionGetter), + baremetalv1.OpenStackBaremetalSetProvServerReadyCondition, + corev1.ConditionFalse, + ) + + }) + }) + +}) diff --git a/tests/functional/suit_test.go b/tests/functional/suit_test.go new file mode 100644 index 0000000..8ab7d68 --- /dev/null +++ b/tests/functional/suit_test.go @@ -0,0 +1,183 @@ +/* +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 functional + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + metal3v1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + //revive:disable-next-line:dot-imports + . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + "github.com/openstack-k8s-operators/openstack-baremetal-operator/controllers" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + k8sClient client.Client // You'll be using this client in your tests. + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc + logger logr.Logger + th *TestHelper + namespace string +) + +const ( + timeout = 40 * time.Second + // have maximum 100 retries before the timeout hits + interval = timeout / 100 +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "crd", "bases"), + filepath.Join("metal3-crds"), + }, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + // NOTE(gibi): if localhost is resolved to ::1 (ipv6) then starting + // the webhook fails as it try to parse the address as ipv4 and + // failing on the colons in ::1 + LocalServingHost: "127.0.0.1", + }, + ErrorIfCRDPathMissing: true, + } + + // cfg is defined in this file globally. + cfg, err := testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + // NOTE(gibi): Need to add all API schemas our operator can own. + // Keep this in synch with PlacementAPIReconciler.SetupWithManager, + // otherwise the reconciler loop will silently not start + // in the test env. + err = baremetalv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = metal3v1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = corev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = appsv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme + + logger = ctrl.Log.WithName("---Test---") + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + th = NewTestHelper(ctx, k8sClient, timeout, interval, logger) + Expect(th).NotTo(BeNil()) + + // Start the controller-manager if goroutine + webhookInstallOptions := &testEnv.WebhookInstallOptions + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + WebhookServer: webhook.NewServer( + webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + }) + Expect(err).ToNot(HaveOccurred()) + err = (&baremetalv1.OpenStackBaremetalSet{}).SetupWebhookWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + err = (&baremetalv1.OpenStackProvisionServer{}).SetupWebhookWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + + kclient, err := kubernetes.NewForConfig(cfg) + Expect(err).ToNot(HaveOccurred(), "failed to create kclient") + err = (&controllers.OpenStackBaremetalSetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + err = (&controllers.OpenStackProvisionServerReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = BeforeEach(func() { + // NOTE(gibi): We need to create a unique namespace for each test run + // as namespaces cannot be deleted in a locally running envtest. See + // https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation + namespace = uuid.New().String() + th.CreateNamespace(namespace) + // We still request the delete of the Namespace to properly cleanup if + // we run the test in an existing cluster. + DeferCleanup(th.DeleteNamespace, namespace) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +})