Skip to content

ci: define basic e2e tests - #1118

Draft
andyatmiami wants to merge 1 commit into
kubeflow:notebooks-v2from
andyatmiami:test/e2e-cypress-tests
Draft

ci: define basic e2e tests#1118
andyatmiami wants to merge 1 commit into
kubeflow:notebooks-v2from
andyatmiami:test/e2e-cypress-tests

Conversation

@andyatmiami

Copy link
Copy Markdown
Contributor

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.

@github-project-automation github-project-automation Bot moved this to Needs Triage in Kubeflow Notebooks May 22, 2026
@google-oss-prow google-oss-prow Bot added the area/ci area - related to ci label May 22, 2026
@google-oss-prow
google-oss-prow Bot requested review from caponetto and thaorell May 22, 2026 03:12
@google-oss-prow google-oss-prow Bot added area/frontend area - related to frontend components area/v2 area - version - kubeflow notebooks v2 size/XXL labels May 22, 2026
@andyatmiami
andyatmiami force-pushed the test/e2e-cypress-tests branch 4 times, most recently from 624922e to 48df9ca Compare May 22, 2026 03:56
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had recent activity.
It will be closed if no further activity occurs.
Thank you for your contributions.

Members may comment /lifecycle frozen to prevent this pull request from being marked as stale.

@github-actions

Copy link
Copy Markdown

This pull request has been automatically closed because it has not had recent activity.
You can reopen the PR if you want.

@github-actions github-actions Bot closed this Aug 13, 2026
@github-project-automation github-project-automation Bot moved this from Needs Triage to Done in Kubeflow Notebooks Aug 13, 2026
@christian-heusel

Copy link
Copy Markdown
Member

/reopen

@google-oss-prow google-oss-prow Bot reopened this Aug 13, 2026
@google-oss-prow

Copy link
Copy Markdown

@christian-heusel: Reopened this PR.

Details

In response to this:

/reopen

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.

@github-project-automation github-project-automation Bot moved this from Done to Needs Triage in Kubeflow Notebooks Aug 13, 2026
@google-oss-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from andyatmiami. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@christian-heusel christian-heusel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 😅

Comment on lines +54 to +62
- 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to upload all videos and screenshot in the case of a test failure? 🤔

Comment on lines +20 to +21
env:
TERM: xterm-256color

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 😅

Comment thread testing/e2e/package.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This file should most likely also be added to the dependabot config in order to ensure that it stays up-to-date:

directory: "/workspaces/frontend"

Comment thread testing/Makefile
clean: ## Remove downloaded tool binaries.
rm -rf $(LOCALBIN)
@echo "INFO: '$(LOCALBIN)' successfully cleaned."
rm -rf $(FRONTEND_ENV_LOCAL)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since this is not a directory we can also drop the -r argument (recursive rm is scary 👻 ):

Suggested change
rm -rf $(FRONTEND_ENV_LOCAL)
rm -f $(FRONTEND_ENV_LOCAL)

Comment on lines +23 to +33
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

#763 (comment)

Comment on lines +29 to +48
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be simplified by using variadic parameter expansion & destructuring (applies to a few other places aswell):

Suggested change
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);
}

Comment on lines +77 to +79
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sleep functions are genererally a bit suspicious, why do we need it here and can we replace it with a "native" timeout? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. 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.
  2. 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?

Comment on lines +6 to +9
const CONTROLLER_SAMPLES = path.resolve(
__dirname,
'../../../../../workspaces/controller/manifests/kustomize/samples',
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe it's simpler to somehow figure out the repository root to spare the ../..? 🤔

Not sure if I like this though:

Suggested change
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',
);

Comment on lines +15 to +17
core: kc.makeApiClient(k8s.CoreV1Api),
rbac: kc.makeApiClient(k8s.RbacAuthorizationV1Api),
custom: kc.makeApiClient(k8s.CustomObjectsApi),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment on lines +21 to +35
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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>
@andyatmiami
andyatmiami force-pushed the test/e2e-cypress-tests branch from 48df9ca to eb57635 Compare August 30, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci area - related to ci area/frontend area - related to frontend components area/v2 area - version - kubeflow notebooks v2 do-not-merge/work-in-progress size/XXL

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

2 participants