Skip to content

Commit 5656e20

Browse files
Merge pull request #597 from pmeida/ote-bootstrap
OCPCLOUD-3009: Bootstrap OTE framework
2 parents 33e0119 + 0fb1035 commit 5656e20

129 files changed

Lines changed: 7497 additions & 3171 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.

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,8 @@ FContext("context name", func() { /* tests */ })
182182
- Each controller has a `suite_test.go` that bootstraps an `envtest.Environment`
183183
- See "Running Tests" above for why `make unit` is required
184184
185+
## OpenShift Tests Extension (OTE)
186+
187+
The `openshift-tests-extension/` directory contains a separate Go module that builds the OTE binary (`cluster-capi-operator-tests-ext`). This binary is embedded in the operator image and used by `openshift-tests` to discover and run e2e tests as part of the OpenShift test infrastructure.
188+
189+
See [`openshift-tests-extension/README.md`](openshift-tests-extension/README.md) for the full developer guide covering: how to add tests to OTE, suite/label routing, feature gate requirements, blocking vs informing lifecycle, per-test timeouts, local runs, and CI verification.

Dockerfile.rhel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22 AS builder
22
WORKDIR /go/src/github.com/openshift/cluster-capi-operator
33
COPY . .
4-
RUN make clean build
4+
RUN make clean build && \
5+
gzip -n bin/cluster-capi-operator-tests-ext
56

67
FROM registry.ci.openshift.org/ocp/4.22:base-rhel9
78
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-operator .
89
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-installer .
910
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-controllers .
1011
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/machine-api-migration .
1112
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/crd-compatibility-checker .
13+
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/cluster-capi-operator-tests-ext.gz /usr/bin/cluster-capi-operator-tests-ext.gz
1214

1315
COPY ./manifests /manifests
1416
COPY ./capi-operator-manifests /capi-operator-manifests

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ verify: fmt lint verify-ocp-manifests ## Run formatting and linting checks
3030

3131
test: verify unit ## Run verification and unit tests
3232

33-
build: bin/capi-operator bin/capi-installer bin/capi-controllers bin/machine-api-migration bin/crd-compatibility-checker manifests-gen ## Build all binaries
33+
build: bin/capi-operator bin/capi-installer bin/capi-controllers bin/machine-api-migration bin/crd-compatibility-checker bin/cluster-capi-operator-tests-ext manifests-gen ## Build all binaries
3434

3535
clean:
3636
rm -rf bin/*
@@ -61,6 +61,11 @@ ocp-manifests: manifests-gen ## Generate admission policy profiles for image emb
6161
bin/%: FORCE | bin/
6262
go build -o "$@" "./cmd/$*"
6363

64+
bin/cluster-capi-operator-tests-ext: FORCE | bin/ ## Build tests extension binary
65+
go build -mod=vendor \
66+
-o bin/cluster-capi-operator-tests-ext \
67+
./openshift-tests-extension/cmd/
68+
6469
.PHONY: localtestenv
6570
localtestenv: .localtestenv
6671

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use (
55
./e2e
66
./hack/tools
77
./manifests-gen
8+
./openshift-tests-extension
89
)
910

1011
replace (

hack/vendor.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ go work use -r .
88

99
# Pass 1: tidy all modules
1010
echo "Running go mod tidy for all modules (pass 1)..."
11-
for module in . e2e manifests-gen hack/tools; do
11+
for module in . e2e manifests-gen hack/tools openshift-tests-extension; do
1212
if [ -f "$module/go.mod" ]; then
1313
echo "Tidying $module"
1414
(cd "$module" && go mod tidy)
@@ -21,7 +21,7 @@ go work sync
2121

2222
# Pass 2: re-tidy after sync may have bumped versions
2323
echo "Running go mod tidy for all modules (pass 2)..."
24-
for module in . e2e manifests-gen hack/tools; do
24+
for module in . e2e manifests-gen hack/tools openshift-tests-extension; do
2525
if [ -f "$module/go.mod" ]; then
2626
echo "Tidying $module"
2727
(cd "$module" && go mod tidy)
@@ -30,7 +30,7 @@ done
3030

3131
# Verify all modules
3232
echo "Verifying all modules..."
33-
for module in . e2e manifests-gen hack/tools; do
33+
for module in . e2e manifests-gen hack/tools openshift-tests-extension; do
3434
if [ -f "$module/go.mod" ]; then
3535
echo "Verifying $module"
3636
(cd "$module" && go mod verify)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# OpenShift Tests Extension (OTE)
2+
3+
This directory contains a separate Go module that builds the OTE binary
4+
(`cluster-capi-operator-tests-ext`). The binary is embedded gzipped in the
5+
operator image at `/usr/bin/cluster-capi-operator-tests-ext.gz` and used by
6+
`openshift-tests` to discover and run e2e tests as part of the OpenShift test
7+
infrastructure.
8+
9+
## Adding a test to OTE
10+
11+
Whether a test file is visible to OTE depends entirely on its file suffix:
12+
13+
| File | `make e2e` | OTE (`list tests`) |
14+
|---|---|---|
15+
| `e2e/my_test.go` |||
16+
| `e2e/my.go` |||
17+
| `e2e/my.go` + `//go:build !e2e` |||
18+
19+
**To make a test discoverable by OTE:** write it in a regular `.go` file (not
20+
`_test.go`). No changes to the extension binary are needed — ginkgo
21+
auto-discovery picks it up on the next build.
22+
23+
**To keep a test out of OTE:** use the standard `_test.go` suffix. It still
24+
runs via `make e2e`.
25+
26+
**To make a test run via OTE only (not `make e2e`):** use a regular `.go` file
27+
with `//go:build !e2e`. `make e2e` passes `--tags=e2e` internally which causes
28+
`!e2e` files to be excluded from compilation. The OTE binary is built without
29+
that tag, so the file is included.
30+
31+
## Test labels and suite routing
32+
33+
Labels on a test control which OTE suite it lands in, and therefore which
34+
nightly conformance job picks it up via the `Parents` field:
35+
36+
| Label on test | OTE suite | Parent suite |
37+
|---|---|---|
38+
| none (default) | `capio/parallel` | `openshift/conformance/parallel` |
39+
| `Label("Serial")` | `capio/serial` | `openshift/conformance/serial` |
40+
| `Label("Disruptive")` | `capio/disruptive` | `openshift/disruptive-longrunning` |
41+
42+
A test is included in a periodic run when **both** of these are true:
43+
1. The cluster has the feature set that enables the test's feature gate (e.g. `TechPreviewNoUpgrade`)
44+
2. The job runs the matching parent suite (e.g. `openshift/conformance/serial`)
45+
46+
Example — a serial test that lands in `capio/serial → openshift/conformance/serial`:
47+
48+
```go
49+
// e2e/aws_machineset.go (regular .go, not _test.go — visible to OTE)
50+
var _ = Describe("[sig-cluster-lifecycle][OCPFeatureGate:ClusterAPIMachineManagementAWS] Cluster API AWS", func() {
51+
It("should scale a MachineSet", Label("Serial"), func() {
52+
// test body
53+
})
54+
})
55+
```
56+
57+
Example — a parallel test (no label) that lands in `capio/parallel → openshift/conformance/parallel`:
58+
59+
```go
60+
var _ = Describe("[sig-cluster-lifecycle][OCPFeatureGate:ClusterAPIMachineManagementAWS] Cluster API AWS", func() {
61+
It("should have a running cluster", func() {
62+
// test body
63+
})
64+
})
65+
```
66+
67+
## Feature gate labeling
68+
69+
Every e2e test must carry the appropriate `[OCPFeatureGate:FeatureName]` tag in
70+
its `Describe` block name. This tells `openshift-tests` to skip the test
71+
automatically on clusters where the feature gate is not enabled:
72+
73+
```go
74+
var _ = Describe("[sig-cluster-lifecycle][OCPFeatureGate:ClusterAPIMachineManagementAWS] Cluster API AWS", func() {
75+
It("should run a machine", func() { ... })
76+
})
77+
```
78+
79+
Without this tag, a test runs on every cluster regardless of feature gate
80+
status, which will cause failures on clusters where the feature is not active.
81+
82+
## Blocking vs informing lifecycle
83+
84+
By default every test is `blocking` — a failure causes the suite exit code to
85+
be non-zero. To mark a test as `informing` (failure recorded but non-blocking):
86+
87+
```go
88+
import g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
89+
90+
It("should be stable but not yet required", g.Informing(), func() { ... })
91+
```
92+
93+
Informing failures appear in the JSON output and Sippy dashboards but do not
94+
gate merges or promotions. Use this temporarily to gather stability data before
95+
promoting a test to blocking. Tests must not remain informing indefinitely.
96+
97+
**Important:** `g.Informing()` is only honored when the test runs through the
98+
OTE binary (`run-suite`, `run-test`). When running via `make e2e`, ginkgo has
99+
no concept of informing lifecycle — a failing informing test will still cause
100+
the `make e2e` run to exit with a non-zero code.
101+
102+
103+
## Running locally
104+
105+
`list tests` and `list suites` work without a cluster. `run-suite` and
106+
`run-test` require a reachable OCP cluster because `InitCommonVariables()`
107+
fetches the `cluster` Infrastructure object.
108+
109+
```bash
110+
# Build the binary
111+
make bin/cluster-capi-operator-tests-ext
112+
113+
# List all discovered tests (no cluster needed)
114+
./bin/cluster-capi-operator-tests-ext list tests
115+
116+
# Run a specific suite against a cluster
117+
./bin/cluster-capi-operator-tests-ext run-suite capio/serial
118+
119+
# Run a single test by exact name
120+
./bin/cluster-capi-operator-tests-ext run-test "[sig-cluster-lifecycle]... test name"
121+
```
122+
123+
The `KUBECONFIG` env var is read directly by `config.GetConfig()` — it must
124+
point to a reachable OCP cluster, not just whatever `kubectl` has as
125+
current-context.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
Copyright 2025 Red Hat, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"time"
23+
24+
"github.com/openshift-eng/openshift-tests-extension/pkg/cmd"
25+
e "github.com/openshift-eng/openshift-tests-extension/pkg/extension"
26+
g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
27+
"github.com/spf13/cobra"
28+
29+
e2e "github.com/openshift/cluster-capi-operator/e2e"
30+
)
31+
32+
func main() {
33+
extensionRegistry := e.NewRegistry()
34+
35+
ext := e.NewExtension("openshift", "payload", "cluster-capi-operator")
36+
37+
defaultTimeout := 30 * time.Minute
38+
disruptiveTimeout := 90 * time.Minute
39+
40+
ext.AddSuite(e.Suite{
41+
Name: "capio/parallel",
42+
Qualifiers: []string{`!labels.exists(l, l == "Serial") && !labels.exists(l, l == "Disruptive")`},
43+
Parents: []string{"openshift/conformance/parallel"},
44+
TestTimeout: &defaultTimeout,
45+
})
46+
47+
ext.AddSuite(e.Suite{
48+
Name: "capio/serial",
49+
Qualifiers: []string{`labels.exists(l, l == "Serial") && !labels.exists(l, l == "Disruptive")`},
50+
Parents: []string{"openshift/conformance/serial"},
51+
Parallelism: 1,
52+
TestTimeout: &defaultTimeout,
53+
})
54+
55+
ext.AddSuite(e.Suite{
56+
Name: "capio/disruptive",
57+
Qualifiers: []string{`labels.exists(l, l == "Disruptive")`},
58+
Parents: []string{"openshift/disruptive-longrunning"},
59+
Parallelism: 1,
60+
ClusterStability: e.ClusterStabilityDisruptive,
61+
TestTimeout: &disruptiveTimeout,
62+
})
63+
64+
specs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite()
65+
if err != nil {
66+
panic(fmt.Sprintf("couldn't build extension test specs from ginkgo: %v", err))
67+
}
68+
69+
specs.AddBeforeAll(func() {
70+
e2e.InitCommonVariables()
71+
})
72+
73+
ext.AddSpecs(specs)
74+
75+
extensionRegistry.Register(ext)
76+
77+
root := &cobra.Command{
78+
Long: "Cluster CAPI Operator tests extension for OpenShift",
79+
}
80+
root.AddCommand(cmd.DefaultExtensionCommands(extensionRegistry)...)
81+
82+
if err := root.Execute(); err != nil {
83+
os.Exit(1)
84+
}
85+
}

0 commit comments

Comments
 (0)