Skip to content

Commit 40c3876

Browse files
jadhajclaude
andcommitted
Add OpenShift Tests Extension (OTE) for baremetal E2E tests
Migrate baremetal E2E tests from openshift-tests-private to cluster-baremetal-operator using the OpenShift Tests Extension (OTE) framework. This enables the tests to run as part of the release payload. Key changes: 1. Test Extension as Separate Go Module - Created cmd/cluster-baremetal-tests-ext/ with dedicated go.mod/vendor - Isolates OTE dependencies from main operator module - Includes 6 baremetal deployment sanity tests migrated from openshift-tests-private 2. OTE Framework Integration - Registered suite: cluster-baremetal/all - Parent suite: openshift/conformance/parallel - Platform filter: baremetal - Uses OpenShift Ginkgo fork with 31 Kubernetes replace directives 3. Build and Packaging - Dockerfile: Added RUN make build-tests and COPY for test binary - Makefile: Added build-tests target with GONOSUMDB and GO_COMPLIANCE_POLICY settings - Binary compressed with gzip for container image 4. Test Path Structure - Moved tests to test/e2e/openshift/baremetal/ to avoid conflicts with upstream BMO/IrSO tests 5. Security Fixes - Fixed CVE-2026-29181: Upgraded OpenTelemetry from v1.37.0 to v1.41.0 in test extension module - Fixed gosec G703 path traversal warnings in hack/readme_inputs/main.go - Replaced shell command execution with native Go file reading in clusterOperatorHealthcheck function 6. CI Compatibility Fixes - Regenerated manifests to fix generate-check checksum mismatch - Fixed vendor case-sensitivity: renamed Microsoft → microsoft (224 files) for Linux CI compatibility - Pinned kustomize to v4.5.4 to fix gnostic API incompatibility - Cleaned up unused golangci-lint and kustomize dependencies (~200 transitive deps) from main go.mod 7. Dependency Management - Main module go.mod reverted to pre-OTE state (OpenTelemetry v1.35.0) - Test extension maintains separate dependencies with security fixes - Vendor directories synchronized with go.mod/go.sum Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6a7e66f commit 40c3876

35,584 files changed

Lines changed: 7924551 additions & 649240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22 AS
44
WORKDIR /go/src/github.com/openshift/cluster-baremetal-operator
55
COPY . .
66
RUN make build
7+
RUN make build-tests
78

89
FROM registry.ci.openshift.org/ocp/4.22:base-rhel9
910
COPY --from=builder /go/src/github.com/openshift/cluster-baremetal-operator/bin/cluster-baremetal-operator /usr/bin/cluster-baremetal-operator
11+
COPY --from=builder /go/src/github.com/openshift/cluster-baremetal-operator/bin/cluster-baremetal-tests-ext.gz /usr/bin/cluster-baremetal-tests-ext.gz
1012
COPY --from=builder /go/src/github.com/openshift/cluster-baremetal-operator/manifests /manifests
1113
LABEL io.openshift.release.operator=true
1214
ENTRYPOINT ["/usr/bin/cluster-baremetal-operator"]

Makefile

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ CONTAINER_TOOL ?= docker
1010
# Image URL to use all building/pushing image targets
1111
IMG ?= controller:latest
1212

13-
CONTROLLER_GEN ?= go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go
13+
CONTROLLER_GEN ?= GOFLAGS=-mod=mod go run sigs.k8s.io/controller-tools/cmd/controller-gen
1414
CRD_OPTIONS="crd:crdVersions=v1"
15-
GOLANGCI_LINT ?= GOLANGCI_LINT_CACHE=$(GOLANGCI_LINT_CACHE) go run vendor/github.com/golangci/golangci-lint/v2/cmd/golangci-lint/main.go
16-
KUSTOMIZE ?= go run sigs.k8s.io/kustomize/kustomize/v4
15+
GOLANGCI_LINT ?= GOLANGCI_LINT_CACHE=$(GOLANGCI_LINT_CACHE) GOFLAGS=-mod=mod go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint
16+
KUSTOMIZE ?= GOFLAGS=-mod=mod go run sigs.k8s.io/kustomize/kustomize/v4@v4.5.4
1717
MANIFEST_PROFILE ?= default
1818
TMP_DIR := $(shell mktemp -d -t manifests-$(date +%Y-%m-%d-%H-%M-%S)-XXXXXXXXXX)
1919

@@ -25,8 +25,9 @@ VERBOSE ?= ""
2525
all: generate lint build
2626

2727
# Run unit tests
28+
# Uses GOFLAGS=-mod=mod to ignore vendor/ directory
2829
unit:
29-
go test $(VERBOSE) ./... -coverprofile cover.out
30+
GOFLAGS=-mod=mod go test $(VERBOSE) ./... -coverprofile cover.out
3031

3132
# Run tests
3233
test: generate lint manifests unit
@@ -35,6 +36,21 @@ test: generate lint manifests unit
3536
build:
3637
go build -o bin/cluster-baremetal-operator main.go
3738

39+
# Build OTE test extension binary
40+
# Test extension is a separate Go module in cmd/cluster-baremetal-tests-ext/ with its own go.mod and vendor/
41+
# This keeps OTE dependencies isolated from the main operator module
42+
# GONOSUMDB bypasses checksum verification for origin module - required because origin uses pseudo-versions not in public checksum database
43+
# CGO_ENABLED=0 produces static binary for container compatibility
44+
# GO_COMPLIANCE_POLICY="exempt_all" is OpenShift-specific variable that exempts test binaries from internal compliance checks
45+
# - Used consistently across OpenShift test extensions (see openshift-tests, oc-tests-ext, etc.)
46+
# - Blanket exemption is standard for test-only binaries that don't ship to customers
47+
# - Test extensions run in CI environments only, not in production clusters
48+
build-tests:
49+
cd cmd/cluster-baremetal-tests-ext && \
50+
GONOSUMDB="github.com/openshift/origin" CGO_ENABLED=0 GO_COMPLIANCE_POLICY="exempt_all" \
51+
go build -mod=vendor -o ../../bin/cluster-baremetal-tests-ext .
52+
gzip -f bin/cluster-baremetal-tests-ext
53+
3854
# Run against the configured Kubernetes cluster in ~/.kube/config
3955
run: generate manifests
4056
go run ./main.go -images-json $(IMAGES_JSON)
@@ -100,6 +116,8 @@ manifests: generate
100116
fmt:
101117

102118
# Run go lint against code
119+
# Uses GOFLAGS=-mod=mod to ignore vendor/ directory which may be out of sync with go.mod
120+
# This allows OTE dependencies in go.mod without requiring vendor/ sync
103121
.PHONY: lint
104122
lint:
105123
$(GOLANGCI_LINT) run
@@ -109,9 +127,10 @@ lint:
109127
vet: lint
110128

111129
# Generate code
130+
# Uses GOFLAGS=-mod=mod to ignore vendor/ directory
112131
.PHONY: generate
113132
generate:
114-
go generate -x ./...
133+
GOFLAGS=-mod=mod go generate -x ./...
115134
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=cluster-baremetal-operator webhook paths=./... output:crd:artifacts:config=config/crd/bases
116135
sed -i '/^ controller-gen.kubebuilder.io\/version: (devel)/d' config/crd/bases/*
117136
$(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..."

cmd/cluster-baremetal-tests-ext/go.mod

Lines changed: 350 additions & 0 deletions
Large diffs are not rendered by default.

cmd/cluster-baremetal-tests-ext/go.sum

Lines changed: 876 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
extcmd "github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
9+
e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
10+
et "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests"
11+
g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
12+
13+
// Import test packages to register Ginkgo specs
14+
_ "github.com/openshift/cluster-baremetal-tests-ext/test/e2e/openshift/baremetal"
15+
)
16+
17+
func main() {
18+
// Extension registry
19+
registry := e.NewRegistry()
20+
21+
// Create extension
22+
ext := e.NewExtension(
23+
"openshift", // product
24+
"payload", // type
25+
"cluster-baremetal", // component name
26+
)
27+
28+
// Add suites to the extension
29+
ext.AddSuite(e.Suite{
30+
Name: "cluster-baremetal/all",
31+
Parents: []string{"openshift/conformance/parallel"},
32+
})
33+
34+
// Build test specs from Ginkgo tests automatically
35+
specs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
36+
if err != nil {
37+
fmt.Fprintf(os.Stderr, "couldn't build extension test specs from ginkgo: %v\n", err)
38+
os.Exit(1)
39+
}
40+
41+
// Apply environment selectors - baremetal platform only
42+
// All tests should only run on baremetal platform
43+
specs.Select(et.NameContains("")).
44+
Include(et.PlatformEquals("baremetal"))
45+
46+
// Add specs to extension
47+
ext.AddSpecs(specs)
48+
registry.Register(ext)
49+
50+
// Create root command
51+
rootCmd := &cobra.Command{
52+
Use: "cluster-baremetal-tests-ext",
53+
Short: "Cluster BareMetal Operator Test Extension",
54+
Long: "OpenShift Tests Extension for Cluster BareMetal Operator E2E Tests",
55+
}
56+
57+
// Register OTE subcommands (info, list, run-test, run-suite, etc.)
58+
rootCmd.AddCommand(extcmd.DefaultExtensionCommands(registry)...)
59+
60+
if err := rootCmd.Execute(); err != nil {
61+
os.Exit(1)
62+
}
63+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package baremetal
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
g "github.com/onsi/ginkgo/v2"
8+
o "github.com/onsi/gomega"
9+
compat_otp "github.com/openshift/origin/test/extended/util/compat_otp"
10+
e2e "k8s.io/kubernetes/test/e2e/framework"
11+
)
12+
13+
var _ = g.Describe("[sig-baremetal] INSTALLER IPI for INSTALLER_GENERAL job on BareMetal", func() {
14+
defer g.GinkgoRecover()
15+
var (
16+
oc = compat_otp.NewCLI("baremetal-deployment-sanity", compat_otp.KubeConfigPath())
17+
iaasPlatform string
18+
)
19+
g.BeforeEach(func() {
20+
compat_otp.SkipForSNOCluster(oc)
21+
iaasPlatform = compat_otp.CheckPlatform(oc)
22+
if !(iaasPlatform == "baremetal") {
23+
e2e.Logf("Cluster is: %s", iaasPlatform)
24+
g.Skip("For Non-baremetal cluster , this is not supported!")
25+
}
26+
})
27+
// author: jhajyahy@redhat.com
28+
// port=yes - 99.7% pass rate (724 runs last 60 days)
29+
g.It("Author:jhajyahy-Medium-29146-Verify that all clusteroperators are Available", func() {
30+
g.By("Running oc get clusteroperators")
31+
res, err := checkOperatorsRunning(oc)
32+
o.Expect(err).NotTo(o.HaveOccurred())
33+
o.Expect(res).To(o.BeTrue())
34+
})
35+
36+
// author: jhajyahy@redhat.com
37+
// port=yes - 100.0% pass rate (724 runs last 60 days)
38+
g.It("Author:jhajyahy-Medium-29719-Verify that all nodes are up and running", func() {
39+
g.By("Running oc get nodes")
40+
res, err := checkNodesRunning(oc)
41+
o.Expect(err).NotTo(o.HaveOccurred())
42+
o.Expect(res).To(o.BeTrue())
43+
44+
})
45+
46+
// author: jhajyahy@redhat.com
47+
// port=yes - 99.7% pass rate (724 runs last 60 days)
48+
g.It("Author:jhajyahy-Medium-32361-Verify that deployment exists and is not empty", func() {
49+
g.By("Create new namespace")
50+
oc.SetupProject()
51+
ns32361 := oc.Namespace()
52+
53+
g.By("Create deployment")
54+
deployCreationErr := oc.Run("create").Args("deployment", "deploy32361", "-n", ns32361, "--image", "quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83").Execute()
55+
o.Expect(deployCreationErr).NotTo(o.HaveOccurred())
56+
57+
g.By("Check deployment status is available")
58+
waitForDeployStatus(oc, "deploy32361", ns32361, "True")
59+
status, err := oc.AsAdmin().Run("get").Args("deployment", "-n", ns32361, "deploy32361", "-o=jsonpath={.status.conditions[?(@.type=='Available')].status}").Output()
60+
o.Expect(err).NotTo(o.HaveOccurred())
61+
e2e.Logf("\nDeployment %s Status is %s\n", "deploy32361", status)
62+
o.Expect(status).To(o.Equal("True"))
63+
64+
g.By("Check pod is in Running state")
65+
podName := getPodName(oc, ns32361)
66+
podStatus := getPodStatus(oc, ns32361, podName)
67+
o.Expect(podStatus).To(o.Equal("Running"))
68+
})
69+
70+
// author: jhajyahy@redhat.com
71+
// port=yes - 99.9% pass rate (724 runs last 60 days)
72+
g.It("Author:jhajyahy-Medium-34195-Verify all pods replicas are running on workers only", func() {
73+
g.By("Create new namespace")
74+
oc.SetupProject()
75+
ns34195 := oc.Namespace()
76+
77+
g.By("Determine appropriate replica count based on cluster topology")
78+
// Query control plane topology to handle SNO, compact, and standard clusters
79+
topologyOutput, err := oc.AsAdmin().Run("get").Args("infrastructure", "cluster", "-o=jsonpath={.status.controlPlaneTopology}").Output()
80+
replicasNum := 3 // safe default for standard clusters
81+
82+
if err == nil && topologyOutput != "" {
83+
switch topologyOutput {
84+
case "SingleReplica":
85+
// SNO cluster - single node, limited capacity
86+
replicasNum = 1
87+
case "HighlyAvailable":
88+
// Standard cluster - use schedulable nodes count
89+
schedulableNodes, nodeErr := oc.AsAdmin().Run("get").Args("nodes", "-o=jsonpath={.items[?(@.spec.taints[*].effect!='NoSchedule')].metadata.name}").Output()
90+
if nodeErr == nil && schedulableNodes != "" {
91+
nodeList := strings.Fields(schedulableNodes)
92+
if len(nodeList) > 0 {
93+
replicasNum = len(nodeList) + 1
94+
}
95+
} else {
96+
// Fallback to worker nodes count if schedulable query fails
97+
workerNodes, workerErr := compat_otp.GetClusterNodesBy(oc, "worker")
98+
if workerErr == nil && len(workerNodes) > 0 {
99+
replicasNum = len(workerNodes) + 1
100+
}
101+
}
102+
default:
103+
// External or unknown topology - use safe default
104+
e2e.Logf("Unknown topology %s, using default replica count: %d", topologyOutput, replicasNum)
105+
}
106+
} else {
107+
e2e.Logf("Unable to query control plane topology, using default replica count: %d", replicasNum)
108+
}
109+
110+
g.By(fmt.Sprintf("Create deployment with %d replicas", replicasNum))
111+
deployCreationErr := oc.Run("create").Args("deployment", "deploy34195", "-n", ns34195, fmt.Sprintf("--replicas=%d", replicasNum), "--image", "quay.io/openshifttest/hello-openshift@sha256:4200f438cf2e9446f6bcff9d67ceea1f69ed07a2f83363b7fb52529f7ddd8a83").Execute()
112+
o.Expect(deployCreationErr).NotTo(o.HaveOccurred())
113+
waitForDeployStatus(oc, "deploy34195", ns34195, "True")
114+
115+
g.By("Check deployed pods number is as expected")
116+
pods, err := oc.AsAdmin().Run("get").Args("pods", "-n", ns34195, "--field-selector=status.phase=Running", "-o=jsonpath={.items[*].metadata.name}").Output()
117+
o.Expect(err).NotTo(o.HaveOccurred())
118+
podList := strings.Fields(pods)
119+
o.Expect(len(podList)).To(o.Equal(replicasNum))
120+
121+
g.By("Check pods are deployed on worker nodes only")
122+
for _, pod := range podList {
123+
podNodeName, err := compat_otp.GetPodNodeName(oc, ns34195, pod)
124+
o.Expect(err).NotTo(o.HaveOccurred())
125+
res := compat_otp.IsWorkerNode(oc, podNodeName)
126+
if !res {
127+
e2e.Logf("\nPod %s was deployed on non worker node %s\n", pod, podNodeName)
128+
}
129+
o.Expect(res).To(o.BeTrue())
130+
}
131+
})
132+
133+
// author: jhajyahy@redhat.com
134+
// port=yes - 99.7% pass rate (724 runs last 60 days)
135+
g.It("Author:jhajyahy-Medium-39126-Verify maximum CPU usage limit hasn't reached on each of the nodes", func() {
136+
g.By("Running oc get nodes")
137+
cpuExceededNodes := []string{}
138+
sampling_time, err := getClusterUptime(oc)
139+
o.Expect(err).NotTo(o.HaveOccurred())
140+
nodeNames, nodeErr := oc.AsAdmin().WithoutNamespace().Run("get").Args("nodes", "-o=jsonpath={.items[*].metadata.name}").Output()
141+
o.Expect(nodeErr).NotTo(o.HaveOccurred(), "Failed to execute oc get nodes")
142+
nodes := strings.Fields(nodeNames)
143+
for _, node := range nodes {
144+
cpuUsage := getNodeCpuUsage(oc, node, sampling_time)
145+
if cpuUsage > maxCpuUsageAllowed {
146+
cpuExceededNodes = append(cpuExceededNodes, node)
147+
e2e.Logf("\ncpu usage of exceeded node: %s is %.2f%%", node, cpuUsage)
148+
}
149+
}
150+
o.Expect(cpuExceededNodes).Should(o.BeEmpty(), "These nodes exceed max CPU usage allowed: %s", cpuExceededNodes)
151+
})
152+
153+
// author: jhajyahy@redhat.com
154+
// port=yes - 99.6% pass rate (724 runs last 60 days)
155+
g.It("Author:jhajyahy-Medium-39125-Verify that every node memory is sufficient", func() {
156+
g.By("Running oc get nodes")
157+
outOfMemoryNodes := []string{}
158+
nodeNames, nodeErr := oc.AsAdmin().WithoutNamespace().Run("get").Args("nodes", "-o=jsonpath={.items[*].metadata.name}").Output()
159+
o.Expect(nodeErr).NotTo(o.HaveOccurred(), "Failed to execute oc get nodes")
160+
nodes := strings.Fields(nodeNames)
161+
for _, node := range nodes {
162+
availMem := getNodeavailMem(oc, node)
163+
e2e.Logf("\nAvailable mem of Node %s is %d", node, availMem)
164+
if availMem < minRequiredMemoryInBytes {
165+
outOfMemoryNodes = append(outOfMemoryNodes, node)
166+
e2e.Logf("\nNode %s does not meet minimum required memory %d Bytes ", node, minRequiredMemoryInBytes)
167+
168+
}
169+
}
170+
o.Expect(outOfMemoryNodes).Should(o.BeEmpty(), "These nodes does not meet minimum required memory: %s", outOfMemoryNodes)
171+
})
172+
})

0 commit comments

Comments
 (0)