Skip to content
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

Initial E2E Tests #387

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
163 changes: 163 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
name: "[E2E Tests] Minikube"

on:
workflow_dispatch:
inputs:
theia-cloud-helm-branch:
description: "Theia Cloud Helm Branch to use"
type: string
default: "main"
schedule:
- cron: "0 13 * * 0"

permissions:
contents: read

concurrency:
group: ci-e2e-theia-cloud-minikube-${{ github.ref }}
cancel-in-progress: true

jobs:
runtests:
name: "Run Tests on Minikube (K8s: ${{ matrix.kubernetes }}, Paths: ${{ matrix.paths }}, Ephemeral: ${{ matrix.ephemeral }}, Keycloak: ${{ matrix.keycloak }})"
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
kubernetes: [v1.32.0, v1.31.4, v1.30.8, v1.29.12]
Copy link
Contributor Author

@jfaltermeier jfaltermeier Dec 19, 2024

Choose a reason for hiding this comment

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

I'm not sure how many versions we should include here. Kubernetes typically provides patches for the last 3-4 releases. I think we should at least test the latest version and the oldest one within this range.

It might also be useful to include all versions, especially when updating our Kubernetes client API libraries. In such cases, if something breaks, it would be helpful to pinpoint exactly which versions have the issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it makes sense to include all versions that are not end of life. Thus, I'd just leave it like this :)

paths: [true, false]
ephemeral: [true, false]
keycloak: [true, false]
steps:
- name: Set Helm Branch for Scheduled Runs
if: ${{ github.event_name == 'schedule' }}
run: echo "INPUT_THEIA_CLOUD_HELM_BRANCH=main" >> $GITHUB_ENV

- name: Set Helm Branch for Manual Runs
if: ${{ github.event_name == 'workflow_dispatch' }}
run: echo "INPUT_THEIA_CLOUD_HELM_BRANCH=${{ github.event.inputs.theia-cloud-helm-branch }}" >> $GITHUB_ENV

- name: Checkout Theia Cloud
uses: actions/checkout@v4
with:
path: "./theia-cloud"

- name: Checkout Theia Cloud Helm
uses: actions/checkout@v4
with:
repository: "eclipsesource/theia-cloud-helm"
ref: "${{ env.INPUT_THEIA_CLOUD_HELM_BRANCH }}"
path: "./theia-cloud-helm"

- name: Setup Minikube
uses: manusa/actions-setup-minikube@92af4db914ab207f837251cd53eb7060e6477614
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: add comment which version this commit hash resolves to.

with:
minikube version: v1.33.1
kubernetes version: ${{ matrix.kubernetes }}
github token: ${{ secrets.GITHUB_TOKEN }}
start args: "--force"

- name: Enable Minikube Addons
run: |
minikube addons enable dashboard
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 the dashboard needed for in E2E tests?

minikube addons enable default-storageclass
minikube addons enable ingress
minikube addons enable metrics-server

- name: List Minikube Addons
run: minikube addons list

- name: Patch Ingress to allow snippet annotations and restart
run: |
kubectl -n ingress-nginx patch cm ingress-nginx-controller --patch '{"data":{"allow-snippet-annotations":"true"}}'
kubectl -n ingress-nginx delete pod -l app.kubernetes.io/name=ingress-nginx

# we use the none driver, so minikube should see the same images on the host
- name: Build Theia Cloud Images
run: |
cd theia-cloud
docker build --no-cache -t theiacloud/theia-cloud-service:minikube-ci-e2e -f dockerfiles/service/Dockerfile .
docker build --no-cache -t theiacloud/theia-cloud-operator:minikube-ci-e2e -f dockerfiles/operator/Dockerfile .
docker build --no-cache -t theiacloud/theia-cloud-landing-page:minikube-ci-e2e -f dockerfiles/landing-page/Dockerfile .
docker build --no-cache -t theiacloud/theia-cloud-wondershaper:minikube-ci-e2e -f dockerfiles/wondershaper/Dockerfile .
docker build --no-cache -t theiacloud/theia-cloud-conversion-webhook:minikube-ci-e2e -f dockerfiles/conversion-webhook/Dockerfile .
docker build --no-cache -t theiacloud/theia-cloud-demo:latest -f demo/dockerfiles/demo-theia-docker/Dockerfile demo/dockerfiles/demo-theia-docker/.
docker tag theiacloud/theia-cloud-demo:latest theiacloud/theia-cloud-demo:minikube-ci-e2e
docker build --no-cache -t theiacloud/theia-cloud-activity-demo:minikube-ci-e2e -f demo/dockerfiles/demo-theia-monitor-vscode/Dockerfile demo/dockerfiles/demo-theia-monitor-vscode/.
docker build --no-cache -t theiacloud/theia-cloud-activity-demo-theia:minikube-ci-e2e -f demo/dockerfiles/demo-theia-monitor-theia/Dockerfile .

- name: Get NGINX Ingress Controller Host
id: ingress_info
run: |
INGRESS_HOST=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.spec.clusterIP}')
echo "INGRESS_HOST=$INGRESS_HOST" >> $GITHUB_ENV

- name: Run Terraform
run: |
cd theia-cloud/terraform/ci-configurations
terraform init
terraform apply \
-var="ingress_ip=${{ env.INGRESS_HOST }}" \
-var="use_paths=${{ matrix.paths }}" \
-var="use_ephemeral_storage=${{ matrix.ephemeral }}" \
-var="enable_keycloak=${{ matrix.keycloak }}" \
-auto-approve

- name: List All Services in All Namespaces
run: kubectl get services --all-namespaces

- name: List All AppDefinitions in All Namespaces
run: kubectl get appdefinitions --all-namespaces

- name: List all Pods Images
run: kubectl get pods --all-namespaces -o jsonpath="{.items[*].spec.containers[*].image}" | tr -s '[[:space:]]' '\n' | sort | uniq

- name: Wait for Deployments to be Ready
run: |
kubectl wait --namespace ingress-nginx --for=condition=available deployment/ingress-nginx-controller --timeout=300s
kubectl wait --namespace theiacloud --for=condition=available deployment/conversion-webhook --timeout=300s
kubectl wait --namespace theiacloud --for=condition=available deployment/landing-page-deployment --timeout=300s
kubectl wait --namespace theiacloud --for=condition=available deployment/operator-deployment --timeout=300s
kubectl wait --namespace theiacloud --for=condition=available deployment/service-deployment --timeout=300s

# URLs
# service: servicex
# landing: trynow
# instance: instances
- name: Access NGINX Ingress URL
if: ${{ matrix.paths == false }}
run: |
curl -LkI "https://trynow.${{ env.INGRESS_HOST }}.nip.io/"

- name: Access NGINX Ingress URL
if: ${{ matrix.paths == true }}
run: |
curl -LkI "https://${{ env.INGRESS_HOST }}.nip.io/trynow/"

- name: Get CA cert
run: |
kubectl get secret theia-cloud-ca-key-pair -n cert-manager -o jsonpath='{.data.tls\.crt}' | base64 --decode > ca.crt
ls -al

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies and run tests
run: |
cd theia-cloud/node
npm ci
npm run build -w e2e-tests
export MATRIX_PATHS=${{ matrix.paths }}
export MATRIX_EPHEMERAL=${{ matrix.ephemeral }}
export MATRIX_KEYCLOAK=${{ matrix.keycloak }}
npm run ui-tests -w e2e-tests

- name: Upload Playwright test failure screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: test-failure-screenshots
path: theia-cloud/node/e2e-tests/test-results/**/*.png
retention-days: 7
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.openapi-generator
.openapi-generator-ignore
openapitools.json
.DS_Store
.DS_Store
node/e2e-tests/test-results/.last-run.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ public Optional<PersistentVolume> createAndApplyPersistentVolume(String correlat
e);
return Optional.empty();
}
return client.persistentVolumesClient().loadAndCreate(correlationId, persistentVolumeYaml,
volume -> volume.addOwnerReference(workspace));
return client.persistentVolumesClient().loadAndCreate(correlationId, persistentVolumeYaml);
}

@Override
Expand Down
3 changes: 2 additions & 1 deletion node/configs/license-check-exclusions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"npm/npmjs/-/landing-page/0.1.0": "Theia Cloud Internal Package",
"npm/npmjs/-/testing-page/0.1.0": "Theia Cloud Internal Package"
"npm/npmjs/-/testing-page/0.1.0": "Theia Cloud Internal Package",
"npm/npmjs/-/e2e-tests/0.1.0": "Theia Cloud Internal Package"
}
24 changes: 24 additions & 0 deletions node/e2e-tests/configs/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
testDir: '../lib/tests',
testMatch: ['**/*.js'],
workers: 1,
fullyParallel: false,
// Timeout for each test in milliseconds.
timeout: 60 * 1000,
use: {
baseURL:
process.env.MATRIX_PATHS !== 'true'
? `https://trynow.${process.env.INGRESS_HOST}.nip.io`
: `https://${process.env.INGRESS_HOST}.nip.io/trynow`,
browserName: 'chromium',
permissions: ['clipboard-read'],
screenshot: 'only-on-failure',
ignoreHTTPSErrors: true
},
preserveOutput: 'failures-only',
reporter: [['list']]
};

export default config;
19 changes: 19 additions & 0 deletions node/e2e-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "e2e-tests",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc && npx playwright install chromium",
"lint": "eslint -c ../.eslintrc.js --ext .ts ./src",
"ui-tests": "npm run build && playwright test --config=./configs/playwright.config.ts"
},
"dependencies": {
"@kubernetes/client-node": "^0.22.3",
"@playwright/test": "^1.41.2",
"@theia/playwright": "^1.34.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"cross-env": "^7.0.3"
}
}
6 changes: 6 additions & 0 deletions node/e2e-tests/src/constats.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the file name contains a typo and the file should be named constants.ts?

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const namespace = 'theiacloud';
export const resourceGroup = 'theia.cloud';
export const sessionVersion = 'v1beta8';
export const sessionPlural = 'sessions';
export const workspaceVersion = 'v1beta5';
export const workspacePlural = 'workspaces';
45 changes: 45 additions & 0 deletions node/e2e-tests/src/k8s.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CustomObjectsApi, KubeConfig } from '@kubernetes/client-node';

import { namespace, resourceGroup, sessionPlural, sessionVersion, workspacePlural, workspaceVersion } from './constats';

const kc = new KubeConfig();
kc.loadFromDefault();
export const k8sApi = kc.makeApiClient(CustomObjectsApi);

export async function deleteAllSessions(): Promise<void> {
const sessions: any = await k8sApi.listNamespacedCustomObject(
resourceGroup,
sessionVersion,
namespace,
sessionPlural
);

for (const resource of sessions.body.items) {
await k8sApi.deleteNamespacedCustomObject(
resourceGroup,
sessionVersion,
namespace,
sessionPlural,
resource.metadata.name
);
}
}

export async function deleteAllWorkspaces(): Promise<void> {
const sessions: any = await k8sApi.listNamespacedCustomObject(
resourceGroup,
workspaceVersion,
namespace,
workspacePlural
);

for (const resource of sessions.body.items) {
await k8sApi.deleteNamespacedCustomObject(
resourceGroup,
workspaceVersion,
namespace,
workspacePlural,
resource.metadata.name
);
}
}
73 changes: 73 additions & 0 deletions node/e2e-tests/src/tests/login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from '@playwright/test';

import { deleteAllSessions, deleteAllWorkspaces } from '../k8s';

test.describe('Login', () => {
test.beforeEach(async () => {
deleteAllSessions();
deleteAllWorkspaces();
});

test('should work', async ({ page, baseURL }) => {
test.skip(process.env.MATRIX_KEYCLOAK !== 'true', 'Skipping test because keycloak not enabled');

expect(baseURL).toBeDefined();

/* Click on Login button */
await page.goto(baseURL!);
const loginButton = await page.locator('.App__try-now-button');
await expect(loginButton).toHaveText('Login');
await loginButton.click();
const signInHeading = await page.locator('#kc-page-title');
await expect(signInHeading).toHaveText('Sign in to your account');

/* Enter user data */
await page.fill('#username', 'foo');
await page.fill('#password', 'foo');
await page.click('#kc-login');
await page.waitForLoadState('networkidle');
const launchButton = await page.locator('.App__try-now-button');
await expect(launchButton).toContainText('Launch Theia with Theia Extension Monitor');
});

test('not required when already logged in', async ({ page, baseURL }) => {
test.skip(process.env.MATRIX_KEYCLOAK !== 'true', 'Skipping test because keycloak not enabled');

expect(baseURL).toBeDefined();

/* Click on Login button */
await page.goto(baseURL!);
const loginButton = await page.locator('.App__try-now-button');
await expect(loginButton).toHaveText('Login');
await loginButton.click();
const signInHeading = await page.locator('#kc-page-title');
await expect(signInHeading).toHaveText('Sign in to your account');

/* Enter user data */
await page.fill('#username', 'foo');
await page.fill('#password', 'foo');
await page.click('#kc-login');
await page.waitForLoadState('networkidle');

/* reload page */
await page.reload();
await page.waitForLoadState('networkidle');

/* check button and email */
const launchButton = await page.locator('.App__try-now-button');
await expect(launchButton).toContainText('Launch Theia with Theia Extension Monitor');
const email = await page.locator('#root .App .header p:nth-child(1)');
await expect(email).toHaveText('[email protected]');
});

test('not required when not using Keycloak', async ({ page, baseURL }) => {
test.skip(process.env.MATRIX_KEYCLOAK === 'true', 'Skipping test because keycloak is enabled');

expect(baseURL).toBeDefined();

/* Check no Login Button */
await page.goto(baseURL!);
const launchButton = await page.locator('.App__try-now-button');
await expect(launchButton).toContainText('Launch Theia with Theia Extension Monitor');
});
});
Loading
Loading