Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7ca7174
Run e2e tests in parallel
creydr Apr 15, 2026
ff17aa4
Cleanup only on success
creydr Apr 15, 2026
762d8d2
Collect deployments, functions and configmaps on test failure
creydr Apr 15, 2026
54712a4
Allow parallel execution of e2e tests
creydr Apr 15, 2026
d1f09bf
Fix comment formatting in middleware update test
creydr Apr 15, 2026
dad1264
Use up-to-date Ginkgo in e2e tests
creydr Apr 16, 2026
c7e3b53
Fix merge queue failures by avoiding GitHub API rate limiting in Dock…
creydr Apr 16, 2026
c283948
Fix flaky metrics endpoint E2E test
creydr Apr 16, 2026
788f324
Move metrics tests into separate file
creydr Apr 16, 2026
34e882c
Fix metrics test failure by setting controllerPodName in BeforeEach
creydr Apr 16, 2026
73e0341
Fix artifact collection to not fail if cluster setup failed
creydr Apr 16, 2026
927e469
Add disk space cleanup to E2E tests workflow
creydr Apr 16, 2026
40e9a37
Collect PipelineRun artifacts in E2E test failures
creydr Apr 16, 2026
3789f37
Increase E2E test timeouts to handle resource contention
creydr Apr 16, 2026
a729dc0
Enable fail-fast for E2E tests
creydr Apr 16, 2026
8c2f543
Collect RBAC resources in E2E test artifacts
creydr Apr 16, 2026
52f31c6
Fix PipelineRun log collection in E2E artifacts
creydr Apr 16, 2026
fbe1035
Pre-pull builder images before parallel E2E tests
creydr Apr 16, 2026
665901d
Only pull needed images (we're using only Golang in the tests ATM)
creydr Apr 16, 2026
e1dc613
Fix Eventually timeout race condition in parallel test execution
creydr Apr 16, 2026
d6c70d8
Use default timeout of 6 minutes and shorter custom timeout for metri…
creydr Apr 16, 2026
32333e9
Remove builder image pre-download
creydr Apr 16, 2026
43794c3
Increase default Eventually timeout to 10 minutes
creydr Apr 16, 2026
d6aeb3f
Merge branch 'main' into rework-e2e-tests
creydr Apr 16, 2026
c9f04b0
Add retry logic to func deploy to handle transient network errors
creydr Apr 17, 2026
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
25 changes: 23 additions & 2 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ jobs:
builder: [pack, s2i]
deployer: [knative, raw, keda]
steps:
- name: Free up disk space
run: |
# Remove large packages to free up disk space on GitHub runners
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
# Clean up Docker to start fresh
docker system prune -af --volumes
df -h

- name: Clone the code
uses: actions/checkout@v6

Expand Down Expand Up @@ -65,8 +73,21 @@ jobs:
if: failure()
run: |
mkdir -p /tmp/k8s-artifacts
kubectl logs -n func-operator-system -l control-plane=controller-manager --tail=-1 --all-containers --prefix --timestamps > /tmp/k8s-artifacts/func-operator.log
kubectl get functions -A -o yaml > /tmp/functions.yaml
kubectl logs -n func-operator-system -l control-plane=controller-manager --tail=-1 --all-containers --prefix --timestamps > /tmp/k8s-artifacts/func-operator.log || true
for resource in functions deployments configmaps pipelineruns roles rolebindings; do
kubectl get ${resource} -A -o yaml > /tmp/k8s-artifacts/${resource}.yaml || true
done

# Install Tekton CLI for better PipelineRun log collection
curl -LO https://github.com/tektoncd/cli/releases/download/v0.44.1/tkn_0.44.1_Linux_x86_64.tar.gz
tar xvzf tkn_0.44.1_Linux_x86_64.tar.gz -C /tmp
chmod +x /tmp/tkn

# Collect logs from all pipelineruns using Tekton CLI
/tmp/tkn pipelinerun list -A > /tmp/k8s-artifacts/pipelinerun-list.txt 2>&1 || true
kubectl get pipelineruns -A -o json | jq -r '.items[] | "\(.metadata.namespace) \(.metadata.name)"' | while read ns name; do
/tmp/tkn pipelinerun logs $name -n $ns > /tmp/k8s-artifacts/pipelinerun-logs-${ns}-${name}.log 2>&1 || true
done

- name: Upload Kubernetes artifacts
if: failure()
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ ARG FUNC_CLI_GH_REPO=knative/func
ARG FUNC_CLI_BRANCH=main

# workaround to invalidate cache when func cli repo got updated
ADD https://api.github.com/repos/${FUNC_CLI_GH_REPO}/git/refs/heads/${FUNC_CLI_BRANCH} version.json
# Use git ls-remote instead of GitHub API to avoid rate limiting (60 req/hour for unauthenticated)
# which caused merge queue failures due to multiple concurrent builds
RUN git ls-remote https://github.com/${FUNC_CLI_GH_REPO} refs/heads/${FUNC_CLI_BRANCH} > version.json

WORKDIR /workspace
RUN git clone --branch ${FUNC_CLI_BRANCH} --single-branch --depth 1 https://github.com/${FUNC_CLI_GH_REPO} .
Expand Down
15 changes: 11 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ test: manifests generate fmt vet setup-envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

.PHONY: test-e2e ## Run e2e tests.
test-e2e:
go test -timeout 1h ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="!bundle"
test-e2e: ginkgo
$(GINKGO) -v --timeout=1h --label-filter="!bundle" --fail-fast -p ./test/e2e/

.PHONY: test-e2e-bundle ## Run bundle e2e tests.
test-e2e-bundle: operator-sdk docker-build docker-push bundle bundle-build bundle-push install-olm-in-cluster
OPERATOR_SDK=$(OPERATOR_SDK) BUNDLE_IMG=$(BUNDLE_IMG) go test -timeout 1h ./test/e2e/ -v -ginkgo.v -ginkgo.timeout=1h -ginkgo.label-filter="bundle"
test-e2e-bundle: operator-sdk docker-build docker-push bundle bundle-build bundle-push install-olm-in-cluster ginkgo
OPERATOR_SDK=$(OPERATOR_SDK) BUNDLE_IMG=$(BUNDLE_IMG) $(GINKGO) -v --timeout=1h --label-filter="bundle" --fail-fast ./test/e2e/

.PHONY: install-olm-in-cluster
install-olm-in-cluster: operator-sdk ## Install OLM in cluster if not already installed.
Expand Down Expand Up @@ -277,10 +277,12 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
MOCKERY = $(LOCALBIN)/mockery
GINKGO = $(LOCALBIN)/ginkgo

## Tool Versions
KUSTOMIZE_VERSION ?= v5.6.0
CONTROLLER_TOOLS_VERSION ?= v0.18.0
GINKGO_VERSION ?= v2.28.1
#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}')
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
Expand Down Expand Up @@ -321,6 +323,11 @@ mockery: ${MOCKERY} ## Download mockery locally if necessary.
${MOCKERY}: $(LOCALBIN)
$(call go-install-tool,${MOCKERY},github.com/vektra/mockery/v3,${MOCKERY_VERSION})

.PHONY: ginkgo
ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
$(GINKGO): $(LOCALBIN)
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo,$(GINKGO_VERSION))

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
Expand Down
3 changes: 0 additions & 3 deletions test/e2e/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ var _ = Describe("Bundle", Label("bundle"), Ordered, func() {
testNamespaces []TestNamespace
)

SetDefaultEventuallyTimeout(5 * time.Minute)
SetDefaultEventuallyPollingInterval(time.Second)

BeforeAll(func() {
bundleImage = os.Getenv("BUNDLE_IMG")
Expect(bundleImage).ToNot(BeEmpty(), "BUNDLE_IMG must be given")
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -51,6 +52,11 @@ func TestE2E(t *testing.T) {
var _ = BeforeSuite(func() {
ctx = context.Background()

// Set global timeout for Eventually assertions
// Must be set here (not in Describe blocks) to avoid race conditions in parallel execution
SetDefaultEventuallyTimeout(10 * time.Minute)
SetDefaultEventuallyPollingInterval(1 * time.Second)

// Register the Function API scheme
err := functionsdevv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
Expand Down
190 changes: 1 addition & 189 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ package e2e
import (
"fmt"
"os/exec"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/functions-dev/func-operator/test/utils"
. "github.com/onsi/ginkgo/v2"
)

// namespace where the project is deployed in
Expand All @@ -33,191 +30,6 @@ const namespace = "func-operator-system"
// serviceAccountName created for the project
const serviceAccountName = "func-operator-controller-manager"

// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "func-operator-controller-manager-metrics-service"

// metricsPort is the port of the metrics service providing the managers metrics
const metricsPort = "8080"

var _ = Describe("Manager", func() {
var controllerPodName string

// After each test, check for failures and collect logs, events,
// and pod descriptions for debugging.
AfterEach(func() {
specReport := CurrentSpecReport()
if specReport.Failed() {
By("Fetching controller manager pod logs")
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
controllerLogs, err := utils.Run(cmd)
if err == nil {
_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
} else {
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
}

By("Fetching Kubernetes events")
cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
eventsOutput, err := utils.Run(cmd)
if err == nil {
_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
} else {
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
}

By("Fetching curl-metrics logs")
cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
metricsOutput, err := utils.Run(cmd)
if err == nil {
_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
} else {
_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
}

By("Fetching controller manager pod description")
cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
podDescription, err := utils.Run(cmd)
if err == nil {
fmt.Println("Pod description:\n", podDescription)
} else {
fmt.Println("Failed to describe controller pod")
}
}
})

SetDefaultEventuallyTimeout(2 * time.Minute)
SetDefaultEventuallyPollingInterval(time.Second)

Context("Manager", func() {
It("should run successfully", func() {
By("validating that the controller-manager pod is running as expected")
verifyControllerUp := func(g Gomega) {
// Get the name of the controller-manager pod
cmd := exec.Command("kubectl", "get",
"pods", "-l", "control-plane=controller-manager",
"-o", "go-template={{ range .items }}"+
"{{ if not .metadata.deletionTimestamp }}"+
"{{ .metadata.name }}"+
"{{ \"\\n\" }}{{ end }}{{ end }}",
"-n", namespace,
)

podOutput, err := utils.Run(cmd)
g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
podNames := utils.GetNonEmptyLines(podOutput)
g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
controllerPodName = podNames[0]
g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

// Validate the pod's status
cmd = exec.Command("kubectl", "get",
"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
"-n", namespace,
)
output, err := utils.Run(cmd)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
}
Eventually(verifyControllerUp).Should(Succeed())
})

Context("with curl-metrics-pod", func() {
curlMetricPodName := "curl-metrics"

AfterEach(func() {
cmd := exec.Command("kubectl", "delete", "pod", curlMetricPodName, "-n", namespace, "--ignore-not-found")
_, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred())
})

It("should ensure the metrics endpoint is serving metrics", func() {
By("validating that the metrics service is available")
cmd := exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
_, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

By("waiting for the metrics endpoint to be ready")
verifyMetricsEndpointReady := func(g Gomega) {
cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace)
output, err := utils.Run(cmd)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(output).To(ContainSubstring(metricsPort), "Metrics endpoint is not ready")
}
Eventually(verifyMetricsEndpointReady).Should(Succeed())

By("verifying that the controller manager is serving the metrics server")
verifyMetricsServerStarted := func(g Gomega) {
cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
output, err := utils.Run(cmd)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"),
"Metrics server not yet started")
}
Eventually(verifyMetricsServerStarted).Should(Succeed())

By("creating the curl-metrics pod to access the metrics endpoint")
cmd = exec.Command("kubectl", "run", curlMetricPodName, "--restart=Never",
"--namespace", namespace,
"--image=curlimages/curl:latest",
"--overrides",
fmt.Sprintf(`{
"spec": {
"containers": [{
"name": "curl",
"image": "curlimages/curl:latest",
"command": ["/bin/sh", "-c"],
"args": ["curl -v %s.%s.svc.cluster.local:%s/metrics"],
"securityContext": {
"allowPrivilegeEscalation": false,
"capabilities": {
"drop": ["ALL"]
},
"runAsNonRoot": true,
"runAsUser": 1000,
"seccompProfile": {
"type": "RuntimeDefault"
}
}
}],
"serviceAccount": "%s"
}
}`, metricsServiceName, namespace, metricsPort, serviceAccountName))
_, err = utils.Run(cmd)
Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

By("waiting for the curl-metrics pod to complete.")
verifyCurlUp := func(g Gomega) {
cmd := exec.Command("kubectl", "get", "pods", curlMetricPodName,
"-o", "jsonpath={.status.phase}",
"-n", namespace)
output, err := utils.Run(cmd)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
}
Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())

By("getting the metrics by checking curl-metrics logs")
metricsOutput := getMetricsOutput()
Expect(metricsOutput).To(ContainSubstring(
"controller_runtime_reconcile_total",
))
})
})

// +kubebuilder:scaffold:e2e-webhooks-checks
})
})

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() string {
By("getting the curl-metrics logs")
cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
metricsOutput, err := utils.Run(cmd)
Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
return metricsOutput
}

// logFailedTestDetails logs function resource and controller logs on test failure
func logFailedTestDetails(functionName, functionNamespace string) {
specReport := CurrentSpecReport()
Expand Down
Loading
Loading