Skip to content

Commit 076da23

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 9482738 commit 076da23

13 files changed

+3841
-0
lines changed

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
@@ -6,6 +6,7 @@ import (
66
"os"
77
"testing"
88

9+
"github.com/onsi/ginkgo/v2"
910
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
1011
"github.com/openshift/hypershift/support/releaseinfo"
1112

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

0 commit comments

Comments
 (0)