Skip to content

OPRUN-3941: [OTE] Add webhook tests (without should be tolerant to openshift-service-ca certificate rotation) #442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,45 @@
"source": "openshift:payload:olmv1",
"lifecycle": "blocking",
"environmentSelector": {}
},
{
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working validating webhook",
"labels": {},
"resources": {
"isolation": {}
},
"source": "openshift:payload:olmv1",
"lifecycle": "blocking",
"environmentSelector": {}
},
{
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working mutating webhook",
"labels": {},
"resources": {
"isolation": {}
},
"source": "openshift:payload:olmv1",
"lifecycle": "blocking",
"environmentSelector": {}
},
{
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should have a working conversion webhook",
"labels": {},
"resources": {
"isolation": {}
},
"source": "openshift:payload:olmv1",
"lifecycle": "blocking",
"environmentSelector": {}
},
{
"name": "[sig-olmv1][OCPFeatureGate:NewOLMWebhookProviderOpenshiftServiceCA][Skipped:Disconnected][Serial] OLMv1 operator with webhooks should be tolerant to tls secret deletion",
"labels": {},
"resources": {
"isolation": {}
},
"source": "openshift:payload:olmv1",
"lifecycle": "blocking",
"environmentSelector": {}
}
]
52 changes: 52 additions & 0 deletions openshift/tests-extension/pkg/helpers/catalogs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package helpers

import (
"context"
"fmt"
"time"

//nolint:staticcheck // ST1001: dot-imports for readability
. "github.com/onsi/gomega"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

olmv1 "github.com/operator-framework/operator-controller/api/v1"

"github/operator-framework-operator-controller/openshift/tests-extension/pkg/env"
)

// NewClusterCatalog returns a new ClusterCatalog object.
// It sets the image reference as source.
func NewClusterCatalog(name, imageRef string) *olmv1.ClusterCatalog {
return &olmv1.ClusterCatalog{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: olmv1.ClusterCatalogSpec{
Source: olmv1.CatalogSource{
Type: olmv1.SourceTypeImage,
Image: &olmv1.ImageSource{
Ref: imageRef,
},
},
},
}
}

// ExpectCatalogToBeServing checks that the catalog with the given name is installed
func ExpectCatalogToBeServing(ctx context.Context, name string) {
k8sClient := env.Get().K8sClient
Eventually(func(g Gomega) {
var catalog olmv1.ClusterCatalog
err := k8sClient.Get(ctx, client.ObjectKey{Name: name}, &catalog)
g.Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("failed to get catalog %q", name))

conditions := catalog.Status.Conditions
g.Expect(conditions).NotTo(BeEmpty(), fmt.Sprintf("catalog %q has empty status.conditions", name))

g.Expect(meta.IsStatusConditionPresentAndEqual(conditions, olmv1.TypeServing, metav1.ConditionTrue)).
To(BeTrue(), fmt.Sprintf("catalog %q is not serving", name))
}).WithTimeout(5 * time.Minute).WithPolling(5 * time.Second).Should(Succeed())
}
83 changes: 83 additions & 0 deletions openshift/tests-extension/pkg/helpers/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package helpers

import (
"context"
"errors"
"fmt"
"os/exec"
"strings"

//nolint:staticcheck // ST1001: dot-imports for readability
. "github.com/onsi/ginkgo/v2"
)

// findK8sTool returns "oc" if available, otherwise "kubectl".
// If we are running locally we either prefer to use oc since some tests
// require it, or fallback to kubectl if oc is not available.
func findK8sTool() (string, error) {
tools := []string{"oc", "kubectl"}
for _, t := range tools {
// First check if the tool is available in the PATH.
if _, err := exec.LookPath(t); err != nil {
continue
}
// Verify that the tool is working by checking its version.
if err := exec.Command(t, "version", "--client").Run(); err == nil {
return t, nil
}
Comment on lines +24 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is strictly necessary, but it does ensure that the client can be executed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is good right to ensure that all is fine mainly because we can either run locally

}
return "", fmt.Errorf("no Kubernetes CLI client found (tried %s)",
strings.Join(tools, ", "))
}

// RunK8sCommand runs a Kubernetes CLI command and returns ONLY stdout.
// If the command fails, stderr is included in the returned error (not mixed with stdout).
func RunK8sCommand(ctx context.Context, args ...string) ([]byte, error) {
tool, err := findK8sTool()
if err != nil {
return nil, err
}

cmd := exec.CommandContext(ctx, tool, args...)
out, err := cmd.Output()
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
stderr := strings.TrimSpace(string(ee.Stderr))
if stderr != "" {
return nil, fmt.Errorf("%s %s failed: %w\nstderr:\n%s",
tool, strings.Join(args, " "), err, stderr)
}
}
return nil, fmt.Errorf("%s %s failed: %w",
tool, strings.Join(args, " "), err)
}
return out, nil
}

// RunAndPrint runs a `kubectl/oc` command via RunK8sCommand and writes both stdout and stderr
// to the GinkgoWriter. It also prints the exact command being run.
func RunAndPrint(ctx context.Context, args ...string) {
fmt.Fprintf(GinkgoWriter, "\n[diag] running: %s\n",
strings.Join(quoteArgs(args), " "))
out, err := RunK8sCommand(ctx, args...)
if err != nil {
fmt.Fprintf(GinkgoWriter, "[diag] command failed: %v\n", err)
}
if len(out) > 0 {
fmt.Fprintf(GinkgoWriter, "%s\n", string(out))
}
}

func quoteArgs(args []string) []string {
quoted := make([]string, len(args))
for i, a := range args {
// Add quotes only if whitespace or special chars are present
if strings.ContainsAny(a, " \t") {
quoted[i] = fmt.Sprintf("%q", a)
} else {
quoted[i] = a
}
Comment on lines +75 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

}
return quoted
}
113 changes: 113 additions & 0 deletions openshift/tests-extension/pkg/helpers/dump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package helpers

import (
"context"
"fmt"
"strings"

//nolint:staticcheck // ST1001: dot-imports for readability
. "github.com/onsi/ginkgo/v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative would be to use g as the import reference. That might improve readability more than using . (and the consequences surrounding that).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IHMO the g is hard to understand because one we need to add g other o. But we are doing it in all files (not only on those), while I agree that can be debatable can we agree taht if we change that would be in a follow up so we can change all places?

)

func sectionHeader(format string, a ...any) {
fmt.Fprintf(GinkgoWriter, "\n=== %s ===\n", fmt.Sprintf(format, a...))
}

func subHeader(format string, a ...any) {
fmt.Fprintf(GinkgoWriter, "\n--- %s ---\n", fmt.Sprintf(format, a...))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I almost want the --- and === to be the same length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

}

// GetAllPodLogs prints logs for all containers in all pods in the given namespace.
func GetAllPodLogs(ctx context.Context, namespace string) {
sectionHeader("[pod-logs] namespace=%s", namespace)

By("listing pods in namespace " + namespace)
namesOut, err := RunK8sCommand(ctx, "get", "pods", "-n", namespace, "-o", "name")
if err != nil {
fmt.Fprintf(GinkgoWriter, "failed to list pods: %v\n%s\n", err, string(namesOut))
return
}
lines := strings.Fields(strings.TrimSpace(string(namesOut)))
if len(lines) == 0 {
fmt.Fprintln(GinkgoWriter, "(no pods found)")
return
}

for _, res := range lines {
subHeader("logs for %s", res)
logsOut, err := RunK8sCommand(
ctx,
"logs",
"-n", namespace,
"--all-containers",
"--prefix",
"--timestamps",
res,
)
if err != nil {
fmt.Fprintf(GinkgoWriter, "error fetching logs for %s: %v\n%s\n", res, err, string(logsOut))
continue
}
_, _ = GinkgoWriter.Write(logsOut) // ignore write error by design
}
fmt.Fprintln(GinkgoWriter)
}

// DescribePods prints the `kubectl/oc describe pods` output for all pods in a given namespace.
func DescribePods(ctx context.Context, namespace string) {
sectionHeader("[describe pods] namespace=%s", namespace)
RunAndPrint(ctx, "describe", "pods", "-n", namespace)
}

// DescribeAllClusterCatalogs lists all ClusterCatalogs and runs `describe` on each.
func DescribeAllClusterCatalogs(ctx context.Context) {
sectionHeader("[cluster catalogs]")

out, err := RunK8sCommand(ctx, "get", "clustercatalogs", "-o", "name")
if err != nil {
fmt.Fprintf(GinkgoWriter, "failed to list clustercatalogs: %v\n", err)
return
}

catalogs := strings.Fields(strings.TrimSpace(string(out)))
if len(catalogs) == 0 {
fmt.Fprintln(GinkgoWriter, "(no clustercatalogs found)")
RunAndPrint(ctx, "get", "clustercatalogs")
return
}

for _, catalog := range catalogs {
subHeader("describe %s", catalog)
RunAndPrint(ctx, "describe", catalog)
}
fmt.Fprintln(GinkgoWriter)
}

// DescribeAllClusterExtensions describes every ClusterExtension in the given namespace.
func DescribeAllClusterExtensions(ctx context.Context, namespace string) {
if namespace == "" {
return
}
sectionHeader("[clusterextensions] namespace=%s", namespace)

args := []string{"get", "clusterextensions", "-n", namespace, "-o", "name"}
out, err := RunK8sCommand(ctx, args...)
if err != nil {
fmt.Fprintf(GinkgoWriter, "failed to list clusterextensions: %v\n", err)
RunAndPrint(ctx, args...)
return
}

names := strings.Fields(strings.TrimSpace(string(out)))
if len(names) == 0 {
fmt.Fprintln(GinkgoWriter, "(no clusterextensions found)")
RunAndPrint(ctx, args...)
return
}

for _, n := range names {
subHeader("describe %s", n)
RunAndPrint(ctx, "describe", "clusterextension", strings.TrimPrefix(n, "clusterextension/"), "-n", namespace)
}
fmt.Fprintln(GinkgoWriter)
}
26 changes: 7 additions & 19 deletions openshift/tests-extension/test/olmv1-incompatible.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes to this file are not strictly related to the subject of this PR. It is mostly some cleanup using other APIs (i.e. to find a command-line client), to avoid duplication. The changes are fine. So, while it may not belong as part of this PR, I'm not going to push for it to be separated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this could've used some of the new helpers that were written in this PR (e.g. NewClusterCatalog), but things can be combined later as needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Admittedly, I did write this fairly independently of the rest of the code, where there wasn't that much infrastructure in-place.)

Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1
}
By(fmt.Sprintf("testing against OCP %s", testVersion))

By("finding a k8s client")
cmdLine, err := getK8sCommandLineClient()
Expect(err).To(Succeed())

By("creating a new Namespace")
nsCleanup := createNamespace(nsName)
DeferCleanup(nsCleanup)
Expand All @@ -85,7 +81,7 @@ var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1
DeferCleanup(fileCleanup)
By(fmt.Sprintf("created operator tarball %q", fileOperator))

By(fmt.Sprintf("starting the operator build with %q via RAW URL", cmdLine))
By("starting the operator build via RAW URL")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is RAW URL?

Copy link
Contributor Author

@camilamacedo86 camilamacedo86 Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A RAW URL usually means a direct link to the unprocessed (raw) content of a file, without any extra formatting, preview, or surrounding interface.

Example:

The change here is just because we are using the utils, so we don't need or have cmdLine anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requested in the PR to add the follow ups (which is in hold because we cannot add the test that fails): #430 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this particular case, it is invoking a k8s API via a URL (using --raw) rather than allowing the client (kubectl or oc) to determine the URL to use. This is needed because building an image is a specific Openshift operation not supported by kubectl. Although it is supported by oc, I wanted to make it generic enough to allow kubectl to be used. Because of this raw URL use, we can't directly use the k8sClient API.

opArgs := []string{
"create",
"--raw",
Expand All @@ -96,7 +92,7 @@ var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1
"-f",
fileOperator,
}
buildOperator := startBuild(cmdLine, opArgs...)
buildOperator := startBuild(opArgs...)

By(fmt.Sprintf("waiting for the build %q to finish", buildOperator.Name))
waitForBuildToFinish(ctx, buildOperator.Name, nsName)
Expand All @@ -114,7 +110,7 @@ var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1
DeferCleanup(fileCleanup)
By(fmt.Sprintf("created catalog tarball %q", fileCatalog))

By(fmt.Sprintf("starting the catalog build with %q via RAW URL", cmdLine))
By("starting the catalog build via RAW URL")
catalogArgs := []string{
"create",
"--raw",
Expand All @@ -125,7 +121,7 @@ var _ = Describe("[sig-olmv1][OCPFeatureGate:NewOLM][Skipped:Disconnected] OLMv1
"-f",
fileCatalog,
}
buildCatalog := startBuild(cmdLine, catalogArgs...)
buildCatalog := startBuild(catalogArgs...)

By(fmt.Sprintf("waiting for the build %q to finish", buildCatalog.Name))
waitForBuildToFinish(ctx, buildCatalog.Name, nsName)
Expand Down Expand Up @@ -386,18 +382,10 @@ func waitForClusterOperatorUpgradable(ctx SpecContext, name string) {
}).WithTimeout(5 * time.Minute).WithPolling(1 * time.Second).Should(Succeed())
}

func getK8sCommandLineClient() (string, error) {
s, err := exec.LookPath("kubectl")
if err != nil {
s, err = exec.LookPath("oc")
}
return s, err
}

func startBuild(cmdLine string, args ...string) *buildv1.Build {
cmd := exec.Command(cmdLine, args...)
output, err := cmd.Output()
func startBuild(args ...string) *buildv1.Build {
output, err := helpers.RunK8sCommand(context.Background(), args...)
Expect(err).To(Succeed(), printExitError(err))

/* The output is JSON of a build.build.openshift.io resource */
build := &buildv1.Build{}
Expect(json.Unmarshal(output, build)).To(Succeed(), "failed to unmarshal build")
Expand Down
Loading