Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions test/e2e/catalogd/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/cucumber/godog"

"github.com/operator-framework/operator-controller/test/internal/e2e"
catalogsteps "github.com/operator-framework/operator-controller/test/internal/e2e/catalogd/steps"
"github.com/operator-framework/operator-controller/test/internal/e2e/steps"
)

Expand All @@ -15,7 +16,9 @@ func TestMain(m *testing.M) {
tc.BeforeSuite(steps.BeforeSuite)
}, func(sc *godog.ScenarioContext) {
steps.RegisterSteps(sc)
catalogsteps.RegisterSteps(sc)
steps.RegisterDemoSteps(sc)
catalogsteps.RegisterHooks(sc)
steps.RegisterHooks(sc)
}))
}
3 changes: 3 additions & 0 deletions test/e2e/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/cucumber/godog"

"github.com/operator-framework/operator-controller/test/internal/e2e"
catalogsteps "github.com/operator-framework/operator-controller/test/internal/e2e/catalogd/steps"
"github.com/operator-framework/operator-controller/test/internal/e2e/steps"
)

Expand Down Expand Up @@ -41,7 +42,9 @@ func TestMain(m *testing.M) {
tc.BeforeSuite(steps.BeforeSuite)
}, func(sc *godog.ScenarioContext) {
steps.RegisterSteps(sc)
catalogsteps.RegisterSteps(sc)
steps.RegisterDemoSteps(sc)
catalogsteps.RegisterHooks(sc)
steps.RegisterHooks(sc, steps.RegisterAsciiCastHooks)
}))
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@ import (
"fmt"
"strings"

"k8s.io/component-base/featuregate"
shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps"
)

// catalogdHAFeature gates scenarios that require a multi-node cluster.
// It is set to true in BeforeSuite when the cluster has at least 2 nodes,
// which is the case for the experimental e2e suite (kind-config-2node.yaml)
// but not the standard suite.
const catalogdHAFeature featuregate.Feature = "CatalogdHA"

// CatalogdLeaderPodIsForceDeleted force-deletes the catalogd leader pod to simulate leader loss.
// The pod is identified from sc.LeaderPods["catalogd"] (populated by a prior
// "catalogd is ready to reconcile resources" step). Force-deletion is equivalent to
Expand All @@ -24,14 +18,14 @@ const catalogdHAFeature featuregate.Feature = "CatalogdHA"
// experimental 2-node cluster are control-plane nodes that run etcd — stopping either
// would break etcd quorum and make the API server unreachable for the rest of the test.
func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error {
sc := Scenario(ctx)
sc := shared.Scenario(ctx)
leaderPod := sc.LeaderPods["catalogd"]
if leaderPod == "" {
return fmt.Errorf("catalogd leader pod not found in scenario context; run 'catalogd is ready to reconcile resources' first")
}

logger.Info("Force-deleting catalogd leader pod", "pod", leaderPod)
if _, err := K8sClient(ctx, "delete", "pod", leaderPod, "-n", NamespaceForComponent("catalogd"),
shared.Log().Info("Force-deleting catalogd leader pod", "pod", leaderPod)
if _, err := shared.K8sClient(ctx, "delete", "pod", leaderPod, "-n", shared.NamespaceForComponent("catalogd"),
"--force", "--grace-period=0"); err != nil {
return fmt.Errorf("failed to force-delete catalogd leader pod %q: %w", leaderPod, err)
}
Expand All @@ -42,11 +36,11 @@ func CatalogdLeaderPodIsForceDeleted(ctx context.Context) error {
// identity changes to a pod other than the deleted leader. It updates
// sc.LeaderPods["catalogd"] with the new leader pod name.
func NewCatalogdLeaderIsElected(ctx context.Context) error {
sc := Scenario(ctx)
sc := shared.Scenario(ctx)
oldLeader := sc.LeaderPods["catalogd"]

WaitFor(ctx, func() bool {
holder, err := K8sClient(ctx, "get", "lease", leaseNames["catalogd"], "-n", NamespaceForComponent("catalogd"),
shared.WaitFor(ctx, func() bool {
holder, err := shared.K8sClient(ctx, "get", "lease", "catalogd-operator-lock", "-n", shared.NamespaceForComponent("catalogd"),
"-o", "jsonpath={.spec.holderIdentity}")
if err != nil || holder == "" {
return false
Expand All @@ -56,7 +50,7 @@ func NewCatalogdLeaderIsElected(ctx context.Context) error {
return false
}
sc.LeaderPods["catalogd"] = newPod
logger.Info("New catalogd leader elected", "pod", newPod)
shared.Log().Info("New catalogd leader elected", "pod", newPod)
return true
})
return nil
Expand Down
85 changes: 85 additions & 0 deletions test/internal/e2e/catalogd/steps/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package steps

import (
"context"
"fmt"

"github.com/cucumber/godog"

shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps"
)

// deploymentRestore records the original state of a deployment so it can be
// rolled back after a test that modifies deployment configuration.
type deploymentRestore struct {
Name string // deployment name
Namespace string
containerName string // container to patch (for env var restores)
patchedArgs bool // true when container args were modified (for TLS profile patches)
originalArgs []string // original container args; may be nil if args were unset
originalEnv []string // original env vars as "NAME=VALUE" (for proxy patches)
}

type catalogContextKey struct{}
type catalogContext struct {
proxy *recordingProxy
deploymentRestores []deploymentRestore
}

func catalogScenario(ctx context.Context) *catalogContext {
return ctx.Value(catalogContextKey{}).(*catalogContext)
}

// RegisterHooks installs catalog-specific state and unconditional deployment restoration.
// Register it before shared hooks so modified deployments are restored before resource cleanup.
func RegisterHooks(sc *godog.ScenarioContext) {
sc.Before(func(ctx context.Context, _ *godog.Scenario) (context.Context, error) {
return context.WithValue(ctx, catalogContextKey{}, &catalogContext{}), nil
})
sc.After(func(ctx context.Context, _ *godog.Scenario, err error) (context.Context, error) {
sc := catalogScenario(ctx)
// Stop the in-process recording proxy if one was started.
if sc.proxy != nil {
sc.proxy.stop()
}
// Restore any deployments that were modified during the scenario. Runs
// unconditionally (even on failure) to prevent a misconfigured deployment
// from bleeding into subsequent scenarios. Restored in LIFO order so that
// multiple patches to the same deployment unwind to the true original.
for i := len(sc.deploymentRestores) - 1; i >= 0; i-- {
dr := sc.deploymentRestores[i]
if dr.patchedArgs {
if err2 := patchDeploymentArgs(dr.Namespace, dr.Name, dr.originalArgs); err2 != nil {
shared.Log().Info("Error restoring deployment args", "name", dr.Name, "error", err2)
} else if _, err2 := shared.K8sClient(ctx, "rollout", "status", "-n", dr.Namespace,
fmt.Sprintf("deployment/%s", dr.Name), "--timeout=2m"); err2 != nil {
shared.Log().Info("Timeout waiting for deployment rollout after restore", "name", dr.Name)
}
}
if err2 := restoreDeployment(dr); err2 != nil {
shared.Log().Info("Error restoring deployment env", "deployment", dr.Name, "namespace", dr.Namespace, "error", err2)
}
}

return ctx, err
})
}

// RegisterSteps registers catalogd HA, proxy, and TLS assertions.
func RegisterSteps(sc *godog.ScenarioContext) {
sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS minimum version "([^"]+)"$`, ConfigureDeploymentWithCustomTLSVersion)
sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS version "([^"]+)", ciphers "([^"]+)", and curves "([^"]+)"$`, ConfigureDeploymentWithCustomTLSFull)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.3 connection$`, MetricsEndpointAcceptsTLS13)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection$`, MetricsEndpointRejectsTLS12)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint negotiates cipher "([^"]+)" over TLS 1\.2$`, MetricsEndpointNegotiatesTLS12Cipher)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection offering only cipher "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCipher)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a TLS 1\.2 connection with cipher "([^"]+)" and curve "([^"]+)"$`, MetricsEndpointAcceptsTLS12ConnectionWithCurve)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a TLS 1\.2 connection with cipher "([^"]+)" and only curve "([^"]+)"$`, MetricsEndpointRejectsTLS12ConnectionWithCurve)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint accepts a connection using only curve "([^"]+)"$`, MetricsEndpointAcceptsConnectionUsingCurve)
sc.Step(`^(?i)the "([^"]+)" metrics endpoint rejects a connection using only curve "([^"]+)"$`, MetricsEndpointRejectsConnectionUsingOnlyCurve)
sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY "([^"]+)"$`, ConfigureDeploymentWithHTTPSProxy)
sc.Step(`^(?i)the "([^"]+)" component is configured with HTTPS_PROXY pointing to a recording proxy$`, StartRecordingProxyAndConfigureDeployment)
sc.Step(`^(?i)the recording proxy received a CONNECT request for the catalogd service$`, RecordingProxyReceivedCONNECTForCatalogd)
sc.Step(`^(?i)the catalogd leader pod is force-deleted$`, CatalogdLeaderPodIsForceDeleted)
sc.Step(`^(?i)a new catalogd leader is elected$`, NewCatalogdLeaderIsElected)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"time"

appsv1 "k8s.io/api/apps/v1"

shared "github.com/operator-framework/operator-controller/test/internal/e2e/steps"
)

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -157,7 +159,7 @@ func kindGatewayIP() (string, error) {
// the "default" namespace, which is the address client-go uses to reach the
// API server from inside a pod (via the KUBERNETES_SERVICE_HOST env var).
func kubernetesClusterIP() (string, error) {
ip, err := K8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default",
ip, err := shared.K8sClient(context.Background(), "get", "service", "kubernetes", "-n", "default",
"-o", "jsonpath={.spec.clusterIP}")
if err != nil {
return "", fmt.Errorf("failed to get kubernetes service cluster IP: %w", err)
Expand All @@ -168,7 +170,7 @@ func kubernetesClusterIP() (string, error) {
// getDeploymentContainerEnv returns the environment variables for the named
// container in the given deployment, as a slice of "NAME=VALUE" strings.
func getDeploymentContainerEnv(deploymentName, namespace, containerName string) ([]string, error) {
raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json")
raw, err := shared.K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json")
if err != nil {
return nil, fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err)
}
Expand Down Expand Up @@ -196,7 +198,7 @@ func getDeploymentContainerEnv(deploymentName, namespace, containerName string)
// the JSON Patch "add" operation, which creates the env field if absent.
func setDeploymentEnvVars(deploymentName, namespace, containerName string, env []string) error {
// Fetch the deployment to find the container index.
raw, err := K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json")
raw, err := shared.K8sClient(context.Background(), "get", "deployment", deploymentName, "-n", namespace, "-o", "json")
if err != nil {
return fmt.Errorf("failed to get deployment %s/%s: %w", namespace, deploymentName, err)
}
Expand Down Expand Up @@ -243,12 +245,12 @@ func setDeploymentEnvVars(deploymentName, namespace, containerName string, env [
return fmt.Errorf("failed to marshal patch: %w", err)
}

if _, err := K8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace,
if _, err := shared.K8sClient(context.Background(), "patch", "deployment", deploymentName, "-n", namespace,
"--type=json", fmt.Sprintf("--patch=%s", string(patchBytes))); err != nil {
return fmt.Errorf("failed to patch deployment %s/%s: %w", namespace, deploymentName, err)
}

if _, err := K8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace,
if _, err := shared.K8sClient(context.Background(), "rollout", "status", "deployment", deploymentName, "-n", namespace,
"--timeout=5m"); err != nil {
return fmt.Errorf("rollout of deployment %s/%s did not complete: %w", namespace, deploymentName, err)
}
Expand Down Expand Up @@ -284,12 +286,12 @@ func restoreDeployment(r deploymentRestore) error {
// proxyURL and NO_PROXY to the Kubernetes API server cluster IP, then waits
// for the rollout. The original env is saved for cleanup.
func configureDeploymentProxy(ctx context.Context, component, proxyURL string) error {
sc := Scenario(ctx)
sc := catalogScenario(ctx)

var deployName string
switch component {
case "operator-controller":
deployName = olmDeploymentName
deployName = "operator-controller-controller-manager"
case "catalogd":
deployName = "catalogd-controller-manager"
default:
Expand All @@ -304,21 +306,21 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e
// namespace fields but have no containerName or originalEnv set.
alreadyTracked := false
for _, r := range sc.deploymentRestores {
if r.Name == deployName && r.Namespace == OLMNamespace && r.containerName != "" {
if r.Name == deployName && r.Namespace == shared.OLMNamespace && r.containerName != "" {
alreadyTracked = true
break
}
}

origEnv, err := getDeploymentContainerEnv(deployName, OLMNamespace, "manager")
origEnv, err := getDeploymentContainerEnv(deployName, shared.OLMNamespace, "manager")
if err != nil {
return err
}

if !alreadyTracked {
sc.deploymentRestores = append(sc.deploymentRestores, deploymentRestore{
Name: deployName,
Namespace: OLMNamespace,
Namespace: shared.OLMNamespace,
containerName: "manager",
originalEnv: origEnv,
})
Expand All @@ -335,7 +337,7 @@ func configureDeploymentProxy(ctx context.Context, component, proxyURL string) e

newEnv := addOrReplaceEnvVar(origEnv, "HTTPS_PROXY", proxyURL)
newEnv = addOrReplaceEnvVar(newEnv, "NO_PROXY", k8sIP)
return setDeploymentEnvVars(deployName, OLMNamespace, "manager", newEnv)
return setDeploymentEnvVars(deployName, shared.OLMNamespace, "manager", newEnv)
}

// ---------------------------------------------------------------------------
Expand All @@ -353,7 +355,7 @@ func ConfigureDeploymentWithHTTPSProxy(ctx context.Context, component, proxyURL
// proxy reachable from the cluster via the container-runtime kind network
// gateway, then patches the component deployment to route HTTPS through it.
func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component string) error {
sc := Scenario(ctx)
sc := catalogScenario(ctx)

p, err := newRecordingProxy()
if err != nil {
Expand All @@ -372,7 +374,7 @@ func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component st
}

proxyURL := fmt.Sprintf("http://%s:%s", gatewayIP, port)
logger.Info("Recording proxy started", "url", proxyURL)
shared.Log().Info("Recording proxy started", "url", proxyURL)

return configureDeploymentProxy(ctx, component, proxyURL)
}
Expand All @@ -386,15 +388,15 @@ func StartRecordingProxyAndConfigureDeployment(ctx context.Context, component st
// CONNECT. This is intentional — the step only verifies that operator-controller
// respected HTTPS_PROXY and sent the request through the proxy.
func RecordingProxyReceivedCONNECTForCatalogd(ctx context.Context) error {
sc := Scenario(ctx)
sc := catalogScenario(ctx)
if sc.proxy == nil {
return fmt.Errorf("no recording proxy was started in this scenario")
}

WaitFor(ctx, func() bool {
shared.WaitFor(ctx, func() bool {
for _, h := range sc.proxy.recordedHosts() {
if strings.Contains(h, "catalogd") {
logger.Info("Recording proxy confirmed CONNECT for catalogd", "host", h)
shared.Log().Info("Recording proxy confirmed CONNECT for catalogd", "host", h)
return true
}
}
Expand Down
Loading
Loading