Skip to content

Commit 103d168

Browse files
committed
test(e2e-v2): add ginkgo-based v2 test suite
Introduce a new Ginkgo v2 based end-to-end test suite with improved structure and organization. The new suite includes: - Suite setup with BeforeSuite/AfterSuite hooks for test initialization - Test organization under test/e2e/v2/tests/ directory - Internal utilities and helpers in test/e2e/v2/internal/ - CRD utility functions in test/e2e/util/ for cross-version compatibility This new test framework will enable more maintainable and scalable e2e testing with better parallel execution support and clearer test structure. Signed-off-by: Cesar Wong <[email protected]> Assisted-by: Claude 3.7 Sonnet (via Claude Code)
1 parent 981170c commit 103d168

File tree

16 files changed

+4141
-1
lines changed

16 files changed

+4141
-1
lines changed

Dockerfile.e2e

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ WORKDIR /hypershift
1414

1515
RUN mkdir -p /hypershift/bin /hypershift/hack
1616
COPY --from=builder /hypershift/bin/test-e2e /hypershift/bin/test-e2e
17+
COPY --from=builder /hypershift/bin/test-e2e-v2 /hypershift/bin/test-e2e-v2
1718
COPY --from=builder /hypershift/bin/test-setup /hypershift//bin/test-setup
1819
COPY --from=builder /hypershift/bin/test-reqserving /hypershift/bin/test-reqserving
1920
COPY --from=builder /hypershift/hack/ci-test-e2e.sh /hypershift/hack/ci-test-e2e.sh

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ GOWS=GO111MODULE=on GOWORK=$(shell pwd)/hack/workspace/go.work GOFLAGS=-mod=vend
3838
GO_BUILD_RECIPE=CGO_ENABLED=1 $(GO) build $(GO_GCFLAGS)
3939
GO_CLI_RECIPE=CGO_ENABLED=0 $(GO) build $(GO_GCFLAGS) -ldflags '-extldflags "-static"'
4040
GO_E2E_RECIPE=CGO_ENABLED=1 $(GO) test $(GO_GCFLAGS) -tags e2e -c
41+
GO_E2EV2_RECIPE=CGO_ENABLED=1 $(GO) test $(GO_GCFLAGS) -tags e2ev2 -c
4142
GO_REQSERVING_E2E_RECIPE=CGO_ENABLED=1 $(GO) test $(GO_GCFLAGS) -tags reqserving -c
4243

4344
OUT_DIR ?= bin
@@ -268,7 +269,7 @@ test: generate
268269
$(GO) test -race -parallel=$(NUM_CORES) -count=1 -timeout=30m ./... -coverprofile cover.out
269270

270271
.PHONY: e2e
271-
e2e: reqserving-e2e
272+
e2e: reqserving-e2e e2ev2
272273
$(GO_E2E_RECIPE) -o bin/test-e2e ./test/e2e
273274
$(GO_BUILD_RECIPE) -o bin/test-setup ./test/setup
274275
cd $(TOOLS_DIR); GO111MODULE=on GOFLAGS=-mod=vendor GOWORK=off go build -tags=tools -o ../../bin/gotestsum gotest.tools/gotestsum
@@ -278,6 +279,11 @@ e2e: reqserving-e2e
278279
reqserving-e2e:
279280
CGO_ENABLED=1 $(GO) test $(GO_GCFLAGS) -tags reqserving -c -o bin/test-reqserving ./test/reqserving-e2e
280281

282+
# Build e2e v2 tests
283+
.PHONY: e2ev2
284+
e2ev2:
285+
$(GO_E2EV2_RECIPE) -o bin/test-e2e-v2 ./test/e2e/v2/tests
286+
281287
# Run go fmt against code
282288
.PHONY: fmt
283289
fmt:

test/e2e/util/crd.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package util
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
9+
10+
crclient "sigs.k8s.io/controller-runtime/pkg/client"
11+
)
12+
13+
// HasFieldInCRDSchema checks if a field path exists in the CRD schema by recursively traversing
14+
// the JSONSchemaProps. The fieldPath should be dot-separated (e.g., "spec.platform.gcp").
15+
func HasFieldInCRDSchema(ctx context.Context, client crclient.Client, crdName, fieldPath string) (bool, error) {
16+
crd := &apiextensionsv1.CustomResourceDefinition{}
17+
if err := client.Get(ctx, crclient.ObjectKey{Name: crdName}, crd); err != nil {
18+
return false, fmt.Errorf("failed to get CRD %s: %w", crdName, err)
19+
}
20+
21+
// Find the served version (prefer v1beta1 if available, otherwise use the first served version)
22+
var version *apiextensionsv1.CustomResourceDefinitionVersion
23+
for i := range crd.Spec.Versions {
24+
if crd.Spec.Versions[i].Served {
25+
version = &crd.Spec.Versions[i]
26+
if version.Name == "v1beta1" {
27+
break
28+
}
29+
}
30+
}
31+
if version == nil || version.Schema == nil || version.Schema.OpenAPIV3Schema == nil {
32+
return false, fmt.Errorf("no valid schema found for CRD %s", crdName)
33+
}
34+
35+
// Split the field path into parts
36+
pathParts := strings.Split(fieldPath, ".")
37+
return hasFieldInSchema(version.Schema.OpenAPIV3Schema, pathParts, 0), nil
38+
}
39+
40+
// hasFieldInSchema recursively checks if a field path exists in the JSONSchemaProps
41+
func hasFieldInSchema(schema *apiextensionsv1.JSONSchemaProps, pathParts []string, index int) bool {
42+
if schema == nil || index >= len(pathParts) {
43+
return index == len(pathParts)
44+
}
45+
46+
currentPart := pathParts[index]
47+
48+
// Check properties first
49+
if schema.Properties != nil {
50+
if prop, exists := schema.Properties[currentPart]; exists {
51+
if index == len(pathParts)-1 {
52+
// This is the last part, field exists
53+
return true
54+
}
55+
// Recurse into the property
56+
return hasFieldInSchema(&prop, pathParts, index+1)
57+
}
58+
}
59+
60+
// Bridge into array items before falling back to the combinators.
61+
if schema.Items != nil {
62+
if schema.Items.Schema != nil && hasFieldInSchema(schema.Items.Schema, pathParts, index) {
63+
return true
64+
}
65+
for i := range schema.Items.JSONSchemas {
66+
if hasFieldInSchema(&schema.Items.JSONSchemas[i], pathParts, index) {
67+
return true
68+
}
69+
}
70+
}
71+
72+
// Check AllOf, AnyOf, OneOf - these can contain the field
73+
for i := range schema.AllOf {
74+
if hasFieldInSchema(&schema.AllOf[i], pathParts, index) {
75+
return true
76+
}
77+
}
78+
for i := range schema.AnyOf {
79+
if hasFieldInSchema(&schema.AnyOf[i], pathParts, index) {
80+
return true
81+
}
82+
}
83+
for i := range schema.OneOf {
84+
if hasFieldInSchema(&schema.OneOf[i], pathParts, index) {
85+
return true
86+
}
87+
}
88+
89+
// Check if there's a $ref that we need to follow
90+
// Note: In CRDs, $ref typically points to definitions within the same schema
91+
// For simplicity, we check properties which is the most common case
92+
return false
93+
}

test/e2e/util/version.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/openshift/hypershift/support/releaseinfo"
1111

1212
"github.com/blang/semver"
13+
"github.com/onsi/ginkgo/v2"
1314
)
1415

1516
var (
@@ -60,12 +61,35 @@ func SetReleaseImageVersion(ctx context.Context, latestReleaseImage string, pull
6061
return nil
6162
}
6263

64+
func SetReleaseVersionFromHostedCluster(ctx context.Context, hostedCluster *hyperv1.HostedCluster) error {
65+
if hostedCluster.Status.Version == nil || len(hostedCluster.Status.Version.History) == 0 || hostedCluster.Status.Version.History[0].Version == "" {
66+
fmt.Fprintf(ginkgo.GinkgoWriter, "WARNING: cannot determine release version from HostedCluster")
67+
return nil
68+
}
69+
hcVersion := hostedCluster.Status.Version.History[0].Version
70+
var err error
71+
releaseVersion, err = semver.Parse(hcVersion)
72+
if err != nil {
73+
return fmt.Errorf("error parsing version: %v", err)
74+
}
75+
releaseVersion.Patch = 0
76+
releaseVersion.Pre = nil
77+
releaseVersion.Build = nil
78+
return nil
79+
}
80+
6381
func AtLeast(t *testing.T, version semver.Version) {
6482
if releaseVersion.LT(version) {
6583
t.Skipf("Only tested in %s and later", version)
6684
}
6785
}
6886

87+
func GinkgoAtLeast(version semver.Version) {
88+
if releaseVersion.LT(version) {
89+
ginkgo.Skip(fmt.Sprintf("Only tested in %s and later", version))
90+
}
91+
}
92+
6993
func CPOAtLeast(t *testing.T, version semver.Version, hc *hyperv1.HostedCluster) {
7094
if hc.Status.Version == nil || hc.Status.Version.Desired.Version == "" {
7195
t.Logf("Desired version is not set on the HostedCluster using latestReleaseImage: %s", releaseVersion)

test/e2e/v2/internal/env_vars.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// +build e2ev2
2+
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 internal
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"sort"
23+
"strings"
24+
)
25+
26+
// EnvVarSpec describes an environment variable used by the test suite
27+
type EnvVarSpec struct {
28+
Name string
29+
Description string
30+
Required bool
31+
Default string
32+
}
33+
34+
var (
35+
// envVarRegistry tracks all environment variables used by the test suite
36+
envVarRegistry = make(map[string]EnvVarSpec)
37+
)
38+
39+
// RegisterEnvVar registers an environment variable specification.
40+
// This should be called in init() functions to document environment variables used by the test suite.
41+
// Example:
42+
//
43+
// RegisterEnvVar("MY_VAR", "Description of what this variable does", true)
44+
func RegisterEnvVar(name, description string, required bool) {
45+
envVarRegistry[name] = EnvVarSpec{
46+
Name: name,
47+
Description: description,
48+
Required: required,
49+
}
50+
}
51+
52+
// RegisterEnvVarWithDefault registers an environment variable specification with a default value.
53+
// This should be called in init() functions to document environment variables used by the test suite.
54+
// Example:
55+
//
56+
// RegisterEnvVarWithDefault("MY_VAR", "Description of what this variable does", false, "default-value")
57+
func RegisterEnvVarWithDefault(name, description string, required bool, defaultValue string) {
58+
envVarRegistry[name] = EnvVarSpec{
59+
Name: name,
60+
Description: description,
61+
Required: required,
62+
Default: defaultValue,
63+
}
64+
}
65+
66+
// GetEnvVarValue returns the value of an environment variable, or its default if not set.
67+
// Panics if the environment variable is not registered in the registry.
68+
func GetEnvVarValue(name string) string {
69+
spec, exists := envVarRegistry[name]
70+
if !exists {
71+
panic(fmt.Sprintf("environment variable %q is not registered. Use RegisterEnvVar or RegisterEnvVarWithDefault to register it", name))
72+
}
73+
74+
value := os.Getenv(name)
75+
if value == "" && spec.Default != "" {
76+
return spec.Default
77+
}
78+
return value
79+
}
80+
81+
// PrintEnvVarHelp prints a formatted help message for all registered environment variables
82+
func PrintEnvVarHelp() {
83+
if len(envVarRegistry) == 0 {
84+
fmt.Println("No environment variables are registered.")
85+
return
86+
}
87+
88+
fmt.Println("Environment Variables:")
89+
fmt.Println(strings.Repeat("=", 80))
90+
91+
// Sort by name for consistent output
92+
var names []string
93+
for name := range envVarRegistry {
94+
names = append(names, name)
95+
}
96+
sort.Strings(names)
97+
98+
for _, name := range names {
99+
spec := envVarRegistry[name]
100+
fmt.Printf("\n%s", name)
101+
if spec.Required {
102+
fmt.Print(" (required)")
103+
} else {
104+
fmt.Print(" (optional)")
105+
}
106+
if spec.Default != "" {
107+
fmt.Printf(" [default: %s]", spec.Default)
108+
}
109+
fmt.Printf("\n %s\n", spec.Description)
110+
if currentValue := os.Getenv(name); currentValue != "" {
111+
fmt.Printf(" Current value: %s\n", maskSensitiveValue(name, currentValue))
112+
}
113+
}
114+
fmt.Println()
115+
}
116+
117+
// maskSensitiveValue masks potentially sensitive environment variable values
118+
func maskSensitiveValue(name, value string) string {
119+
lowerName := strings.ToLower(name)
120+
if strings.Contains(lowerName, "secret") ||
121+
strings.Contains(lowerName, "password") ||
122+
strings.Contains(lowerName, "token") ||
123+
strings.Contains(lowerName, "key") {
124+
if len(value) > 8 {
125+
return value[:4] + "..." + value[len(value)-4:]
126+
}
127+
return "****"
128+
}
129+
return value
130+
}
131+
132+
func init() {
133+
// Register environment variables used by the test suite
134+
RegisterEnvVar(
135+
"E2E_HOSTED_CLUSTER_NAME",
136+
"Name of the HostedCluster to test. Required for tests that interact with a hosted cluster.",
137+
false,
138+
)
139+
RegisterEnvVar(
140+
"E2E_HOSTED_CLUSTER_NAMESPACE",
141+
"Namespace of the HostedCluster to test. Required for tests that interact with a hosted cluster.",
142+
false,
143+
)
144+
RegisterEnvVar(
145+
"E2E_SHOW_ENV_HELP",
146+
"When set to any non-empty value, displays environment variable help and exits without running tests.",
147+
false,
148+
)
149+
RegisterEnvVarWithDefault(
150+
"E2E_STRICT_MODE",
151+
"When set to true, enables strict mode validation that requires all pods to belong to predefined workloads.",
152+
false,
153+
"false",
154+
)
155+
}

0 commit comments

Comments
 (0)