ci: define basic e2e tests - #1118
Conversation
624922e to
48df9ca
Compare
|
This pull request has been automatically marked as stale because it has not had recent activity. Members may comment |
|
This pull request has been automatically closed because it has not had recent activity. |
|
/reopen |
|
@christian-heusel: Reopened this PR. DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
christian-heusel
left a comment
There was a problem hiding this comment.
Hey @andyatmiami thanks a lot for driving this forward, I think E2E tests will be more and more important as we move forward! 🚧
I have left you a few review comments, feel free to implement or challenge as you deem sensible!
Also could you rebase your changes? A lot has changed since you have initially posted this 😅
| - name: Upload test artifacts | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| if: failure() | ||
| with: | ||
| name: e2e-test-results | ||
| path: | | ||
| testing/e2e/cypress/videos/ | ||
| testing/e2e/cypress/screenshots/ | ||
| retention-days: 7 |
There was a problem hiding this comment.
Do we need to upload all videos and screenshot in the case of a test failure? 🤔
| env: | ||
| TERM: xterm-256color |
There was a problem hiding this comment.
What is this used for any why do we need it? 🤔 Please add a comment that explains why it is there in case that it is important 😅
There was a problem hiding this comment.
This file should most likely also be added to the dependabot config in order to ensure that it stays up-to-date:
notebooks/.github/dependabot.yml
Line 46 in 9bd0400
| clean: ## Remove downloaded tool binaries. | ||
| rm -rf $(LOCALBIN) | ||
| @echo "INFO: '$(LOCALBIN)' successfully cleaned." | ||
| rm -rf $(FRONTEND_ENV_LOCAL) |
There was a problem hiding this comment.
Since this is not a directory we can also drop the -r argument (recursive rm is scary 👻 ):
| rm -rf $(FRONTEND_ENV_LOCAL) | |
| rm -f $(FRONTEND_ENV_LOCAL) |
| for i in $(seq 1 10); do | ||
| if curl -sk "https://localhost:${GATEWAY_PORT}/workspaces/api/v1/healthcheck" >/dev/null 2>&1; then | ||
| echo "✓ Gateway port-forward ready" | ||
| break | ||
| fi | ||
| if [[ $i -eq 10 ]]; then | ||
| echo "✗ ERROR: Gateway port-forward failed to become ready" | ||
| exit 1 | ||
| fi | ||
| sleep 2 | ||
| done |
There was a problem hiding this comment.
Same review comment as on the other PR, we can do this part purely in curl:
if ! curl --silent --output /dev/null --fail \
--retry 10 --retry-delay 2 --retry-all-errors \
"http://localhost:${local_port}${probe_path}"; then
echo "✗ ERROR: ${label} endpoint not reachable after 10 attempts"
return 1
fi| async function k8sGet(params: K8sResourceParams): Promise<object> { | ||
| const api = getClient(); | ||
| if (params.namespace) { | ||
| const resp = await api.getNamespacedCustomObject({ | ||
| group: params.group, | ||
| version: params.version, | ||
| namespace: params.namespace, | ||
| plural: params.plural, | ||
| name: params.name, | ||
| }); | ||
| return resp; | ||
| } | ||
| const resp = await api.getClusterCustomObject({ | ||
| group: params.group, | ||
| version: params.version, | ||
| plural: params.plural, | ||
| name: params.name, | ||
| }); | ||
| return resp; | ||
| } |
There was a problem hiding this comment.
I think this can be simplified by using variadic parameter expansion & destructuring (applies to a few other places aswell):
| async function k8sGet(params: K8sResourceParams): Promise<object> { | |
| const api = getClient(); | |
| if (params.namespace) { | |
| const resp = await api.getNamespacedCustomObject({ | |
| group: params.group, | |
| version: params.version, | |
| namespace: params.namespace, | |
| plural: params.plural, | |
| name: params.name, | |
| }); | |
| return resp; | |
| } | |
| const resp = await api.getClusterCustomObject({ | |
| group: params.group, | |
| version: params.version, | |
| plural: params.plural, | |
| name: params.name, | |
| }); | |
| return resp; | |
| } | |
| async function k8sGet(params: K8sResourceParams): Promise<object> { | |
| const api = getClient(); | |
| const { namespace } = params; | |
| if (namespace) { | |
| return api.getNamespacedCustomObject({ ...params, namespace }); | |
| } | |
| return api.getClusterCustomObject(params); | |
| } |
| function sleep(ms: number): Promise<void> { | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } |
There was a problem hiding this comment.
Sleep functions are genererally a bit suspicious, why do we need it here and can we replace it with a "native" timeout? 🤔
There was a problem hiding this comment.
However the 🤖 says its a pragmatic way of approaching the issue, so maybe we can just leave it as-is:
Is there an alternative way of implementing k8sWaitForResource() that relies on upstream primitives and timeouts?
Yes — two upstream primitives worth considering instead of the hand-rolled Date.now() polling loop:
- k8s.Watch (from @kubernetes/client-node) opens a watch connection and streams ADDED/MODIFIED events, so you resolve as soon as the resource appears instead of polling every 2s. It's event-driven and typically faster/lighter, but adds real complexity here: you still need an initial GET (the resource may already exist before the watch starts), you have to handle the watch's own reconnect/error semantics, and cluster-vs-namespaced-scoped watching means building a different path string per case — essentially duplicating the k8sGet branching one level down.
- AbortSignal.timeout(ms), passed as abortSignal into the API call options, would let you drop your manual deadline/Date.now() bookkeeping and instead just catch the abort error — but that only replaces the timeout arithmetic, not the poll loop itself, since you'd still need to retry the GET on 404/abort until success.
My take: the current poll-and-isNotFoundError loop is the pragmatic choice for a test helper — it's ~15 lines, easy to reason about, and "wait up to N seconds for a CRD to exist" isn't performance-sensitive in e2e tests. I'd only reach for Watch if polling interval latency were actually causing flaky/slow tests. Want me to prototype the Watch-based version so you can compare?
| const CONTROLLER_SAMPLES = path.resolve( | ||
| __dirname, | ||
| '../../../../../workspaces/controller/manifests/kustomize/samples', | ||
| ); |
There was a problem hiding this comment.
Maybe it's simpler to somehow figure out the repository root to spare the ../..? 🤔
Not sure if I like this though:
| const CONTROLLER_SAMPLES = path.resolve( | |
| __dirname, | |
| '../../../../../workspaces/controller/manifests/kustomize/samples', | |
| ); | |
| function findRepoRoot(startDir: string): string { | |
| let dir = startDir; | |
| while (!fs.existsSync(path.join(dir, '.git'))) { | |
| const parent = path.dirname(dir); | |
| if (parent === dir) { | |
| throw new Error(`Could not find repository root above ${startDir}`); | |
| } | |
| dir = parent; | |
| } | |
| return dir; | |
| } | |
| const CONTROLLER_SAMPLES = path.join( | |
| findRepoRoot(__dirname), | |
| 'workspaces/controller/manifests/kustomize/samples', | |
| ); |
| core: kc.makeApiClient(k8s.CoreV1Api), | ||
| rbac: kc.makeApiClient(k8s.RbacAuthorizationV1Api), | ||
| custom: kc.makeApiClient(k8s.CustomObjectsApi), |
There was a problem hiding this comment.
Do we perhaps also need https://kubernetes-client.github.io/javascript/api-reference/workloads/AppsV1Api?_highlight=state#stateful-set-1 here? 🤔
| function isConflictError(err: unknown): boolean { | ||
| if (err instanceof Error && err.message.includes('HTTP-Code: 409')) { | ||
| return true; | ||
| } | ||
| const httpErr = err as { statusCode?: number }; | ||
| return httpErr.statusCode === 409; | ||
| } | ||
|
|
||
| function isNotFoundError(err: unknown): boolean { | ||
| if (err instanceof Error && err.message.includes('HTTP-Code: 404')) { | ||
| return true; | ||
| } | ||
| const httpErr = err as { statusCode?: number }; | ||
| return httpErr.statusCode === 404; | ||
| } |
There was a problem hiding this comment.
Same comment as above (https://github.com/kubeflow/notebooks/pull/1118/changes#r3856211135) we could potentially use the same solution here if we like it 😊
Add a self-contained Cypress e2e test suite under testing/e2e/ that validates core user workflows against a real Kubernetes cluster. The suite runs two specs: a user creating a Workspace through the wizard, and an admin creating a WorkspaceKind via YAML upload. Kubernetes environment setup (namespace, RBAC, seed data) is handled programmatically via @kubernetes/client-node in Cypress plugin tasks rather than shell scripts and static YAML manifests. A global before() hook calls setupE2e() before any spec runs, making the test project self-contained with all dependencies co-located. The JupyterLab WorkspaceKind seed data references the controller's sample YAML directly to avoid maintaining duplicate fixtures. Authentication is simulated by injecting the kubeflow-userid header via cy.intercept(), with separate loginAsAdmin and loginAsUser helpers that map to distinct RBAC policies (cluster-scoped admin vs namespace-scoped user). Custom Cypress commands (k8sGet, k8sDelete, k8sWaitForResource) provide a thin wrapper over the Kubernetes API for in-test assertions and cleanup. Each spec deletes resources it creates in afterEach() hooks. Page objects encapsulate UI interactions for the workspaces list, workspace creation wizard, workspace kinds list, and workspace kind creation form. The CI workflow deploys all three components (controller, backend, frontend) to a Kind cluster with Istio and cert-manager, runs a sanity check to verify endpoints respond, then executes Cypress with the gateway port-forwarded. The frontend is built in standalone deployment mode via .env.production.local so it serves without the central dashboard. Test artifacts (videos, screenshots) are uploaded on failure. The Makefile provides an `e2e` convenience target that chains setup-cluster, deploy-all, sanity-check, and local-e2e for local development. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Andy Stoneberg <astonebe@redhat.com>
48df9ca to
eb57635
Compare
Add a self-contained Cypress e2e test suite under testing/e2e/ that validates core user workflows against a real Kubernetes cluster. The suite runs two specs: a user creating a Workspace through the wizard, and an admin creating a WorkspaceKind via YAML upload.
Kubernetes environment setup (namespace, RBAC, seed data) is handled programmatically via @kubernetes/client-node in Cypress plugin tasks rather than shell scripts and static YAML manifests. A global before() hook calls setupE2e() before any spec runs, making the test project self-contained with all dependencies co-located. The JupyterLab WorkspaceKind seed data references the controller's sample YAML directly to avoid maintaining duplicate fixtures.
Authentication is simulated by injecting the kubeflow-userid header via cy.intercept(), with separate loginAsAdmin and loginAsUser helpers that map to distinct RBAC policies (cluster-scoped admin vs namespace-scoped user).
Custom Cypress commands (k8sGet, k8sDelete, k8sWaitForResource) provide a thin wrapper over the Kubernetes API for in-test assertions and cleanup. Each spec deletes resources it creates in afterEach() hooks.
Page objects encapsulate UI interactions for the workspaces list, workspace creation wizard, workspace kinds list, and workspace kind creation form.
The CI workflow deploys all three components (controller, backend, frontend) to a Kind cluster with Istio and cert-manager, runs a sanity check to verify endpoints respond, then executes Cypress with the gateway port-forwarded. The frontend is built in standalone deployment mode via .env.production.local so it serves without the central dashboard. Test artifacts (videos, screenshots) are uploaded on failure.
The Makefile provides an
e2econvenience target that chains setup-cluster, deploy-all, sanity-check, and local-e2e for local development.