Skip to content
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
82 changes: 80 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ concurrency:
cancel-in-progress: true
permissions:
contents: read # for actions/checkout to fetch code

env:
DEBUG: false

jobs:
golangci:
name: lint
Expand Down Expand Up @@ -89,20 +93,37 @@ jobs:
large-packages: false
docker-images: false
swap-storage: false

- name: Checkout karmada repo
uses: actions/checkout@v4
with:
repository: karmada-io/karmada
path: karmada

- name: setup e2e test environment
run: |
cd karmada
export CLUSTER_VERSION=kindest/node:${{ matrix.k8s }}
hack/local-up-karmada.sh
echo "KUBECONFIG=/home/runner/.kube/karmada.config" >> $GITHUB_ENV
- name: Debug cluster status
Copy link
Contributor

Choose a reason for hiding this comment

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

could optimize the debug code or adopt the debug code with debug var

if: ${{ env.DEBUG == 'true' }}
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

run: |
kubectl cluster-info || echo "Cluster info failed"
kubectl get nodes -o wide || echo "Get nodes failed"
kubectl get pods -A || echo "Get pods failed"
kind get clusters || echo "Kind get clusters failed"
- name: Checkout dashboard repo
uses: actions/checkout@v4
with:
path: karmada-dashboard

- name: Generate KARMADA_TOKEN for CI
run: |
kubectl --context=karmada-host apply -k karmada-dashboard/artifacts/overlays/nodeport-mode
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we can reuse the debug var or just drop lines

kubectl --context=karmada-apiserver apply -f karmada-dashboard/artifacts/dashboard/karmada-dashboard-sa.yaml
TOKEN=$(kubectl --context=karmada-apiserver -n karmada-system get secret/karmada-dashboard-secret -o go-template="{{.data.token | base64decode}}")
echo "KARMADA_TOKEN=$TOKEN" >> $GITHUB_ENV
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
Expand All @@ -111,25 +132,82 @@ jobs:
with:
# keep in sync with the packageManager version in `ui/package.json`
version: 9.1.2

- name: Install dependencies
run: |
echo "Start build"
pnpm --version
cd karmada-dashboard/ui
pnpm install
pnpm turbo build
- name: Build API binary
run: |
cd karmada-dashboard
make build
- name: Start API server
run: |
cd karmada-dashboard
make run-api &
echo $! > .api.pid
sleep 5
- name: Wait for API server to be ready
run: |
echo "Waiting for API server to be ready..."
timeout 60s bash -c 'while ! nc -z localhost 8000; do echo "Waiting for API server port..."; sleep 5; done' || {
echo "API server failed to start within 60 seconds"
echo "=== API Server Debug ==="
if [ -f karmada-dashboard/.api.pid ]; then
API_PID=$(cat karmada-dashboard/.api.pid)
ps aux | grep $API_PID || echo "API process not found"
fi
ss -tuln | grep 8000 || echo "Port 8000 not listening"
exit 1
}
echo "API server is ready!"
- name: Install Playwright Browsers
run: |
cd karmada-dashboard/ui/apps/dashboard
pnpm exec playwright install --with-deps
- name: Debug environment before starting dashboard
if: ${{ env.DEBUG == 'true' }}
run: |
echo "KARMADA_TOKEN is set: ${{ env.KARMADA_TOKEN != '' }}"
echo "KUBECONFIG: $KUBECONFIG"
echo "Current directory: $(pwd)"
echo "Dashboard directory contents:"
ls -la karmada-dashboard/ || echo "No dashboard directory"
echo "Check if Makefile exists:"
ls -la karmada-dashboard/Makefile || echo "No Makefile found"
- name: Build frontend (for e2e)
working-directory: karmada-dashboard/ui/apps/dashboard
run: |
pnpm build
- name: Test API connectivity before running tests
run: |
echo "Testing API connectivity..."
curl -f http://localhost:8000/api/v1/cluster && echo "✓ Cluster API works" || echo "✗ Cluster API failed"
curl -f http://localhost:8000/api/v1/namespace && echo "✓ Namespace API works" || echo "✗ Namespace API failed"
echo "API tests completed"
- name: Debug working dir
if: ${{ env.DEBUG == 'true' }}
run: |
pwd
ls -al
- name: Run Playwright tests
working-directory: karmada-dashboard/ui/apps/dashboard
run: |
cd karmada-dashboard/ui/apps/dashboard
pnpm exec playwright test
- name: Cleanup processes
if: always()
run: |
echo "Cleaning up processes..."
if [ -f karmada-dashboard/.api.pid ]; then
API_PID=$(cat karmada-dashboard/.api.pid)
kill $API_PID 2>/dev/null || echo "API process already stopped"
fi
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.k8s }}
path: karmada-dashboard/ui/apps/dashboard/playwright-report/
retention-days: 30

64 changes: 64 additions & 0 deletions ui/apps/dashboard/e2e/namespace/namespace-create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2024 The Karmada Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// apps/dashboard/e2e/namespace-create.spec.ts
import { test, expect } from '@playwright/test';

// Set webServer.url and use.baseURL with the location of the WebServer
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 5173;
const baseURL = `http://${HOST}:${PORT}`;
const basePath = '/multicloud-resource-manage';
const token = process.env.KARMADA_TOKEN || '';

test.beforeEach(async ({ page }) => {
await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('text=Dashboard', { timeout: 30000 });
});

test('should create a new namespace', async ({ page }) => {
// 打开 Namespaces 页面
await page.waitForSelector('text=Namespaces', { timeout: 60000 });
await page.click('text=Namespaces');

// 点击 "Add" 创建新 namespace
await page.waitForSelector('button:has-text("Add")', { timeout: 30000 });
await page.click('button:has-text("Add")');

// 填写唯一 namespace 名称
const namespaceName = `test-${Date.now()}`;
await page.waitForSelector('#name', { timeout: 30000 });
await page.fill('#name', namespaceName);

// 提交创建
await page.click('label:has-text("No")');
await page.click('button:has-text("Submit")');

// 搜索并验证 namespace 已创建
const searchBox = page.getByPlaceholder('Search by Name');
await searchBox.fill(namespaceName);
await searchBox.press('Enter');
await expect(page.locator('table')).toContainText(namespaceName);

// Debug
if(process.env.DEBUG === 'true'){
await page.screenshot({ path: 'debug-namespace-create.png', fullPage: true });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

good job~

});
96 changes: 96 additions & 0 deletions ui/apps/dashboard/e2e/namespace/namespace-delete.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright 2024 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// apps/dashboard/e2e/namespace-delete.spec.ts
import { test, expect } from '@playwright/test';

// Set webServer.url and use.baseURL with the location of the WebServer
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 5173;
const baseURL = `http://${HOST}:${PORT}`;
const basePath = '/multicloud-resource-manage';
const token = process.env.KARMADA_TOKEN || '';

test.beforeEach(async ({ page }) => {
await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('text=Dashboard', { timeout: 30000 });
});

test('should delete a namespace', async ({ page }) => {
await page.waitForSelector('text=Namespaces', { timeout: 60000 });
await page.click('text=Namespaces');

// 创建临时 namespace
const namespaceName = `test-to-delete-${Date.now()}`;
await page.click('button:has-text("Add")');
await page.fill('#name', namespaceName);
await page.click('label:has-text("No")');
await page.click('button:has-text("Submit")');

// 使用搜索框确认创建成功
const searchBox = page.getByPlaceholder('Search by Name');
await searchBox.fill(namespaceName);
await searchBox.press('Enter');
await page.waitForSelector(`tr:has-text("${namespaceName}")`, { timeout: 30000 });

// 删除 namespace
await page.click(`tr:has-text("${namespaceName}") button:has-text("Delete")`);
await page.click('button:has-text("Confirm")');

// 刷新页面,确保表格拉取最新数据
await page.reload({ waitUntil: 'networkidle' });

// 使用搜索框确认删除
await searchBox.fill(namespaceName);
await searchBox.press('Enter');

//确认 namespace 已删除
const table = page.locator('table');
const start = Date.now();
let gone = false;

while (Date.now() - start < 120000) { // 最多等 120 秒
const content = await table.innerText();
if (!content.includes(namespaceName)) {
console.log(`Namespace ${namespaceName} 已彻底删除`);
gone = true;
break;
} else if (content.includes('Terminating')) {
console.log(`Namespace ${namespaceName} Terminating`);
} else {
console.log(`Namespace ${namespaceName} 仍然存在`);
}
await page.waitForTimeout(5000); // 每 5 秒检查一次
await page.reload({ waitUntil: 'networkidle' }); // 强制刷新,拿最新数据
await searchBox.fill(namespaceName);
await searchBox.press('Enter');
}

// 确认最终被删除(如果超时则失败)
expect(gone).toBeTruthy();

// 清空搜索框
await searchBox.clear();

// Debug
if(process.env.DEBUG === 'true'){
await page.screenshot({ path: 'debug-namespace-delete.png', fullPage: true });
}
});
52 changes: 52 additions & 0 deletions ui/apps/dashboard/e2e/namespace/namespace-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2024 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// apps/dashboard/e2e/namespace-list.spec.ts
import { test, expect } from '@playwright/test';

// Set webServer.url and use.baseURL with the location of the WebServer
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 5173;
const baseURL = `http://${HOST}:${PORT}`;
const basePath = '/multicloud-resource-manage';
const token = process.env.KARMADA_TOKEN || '';

test.beforeEach(async ({ page }) => {
await page.goto(`${baseURL}/login`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.goto(`${baseURL}${basePath}`, { waitUntil: 'networkidle' });
await page.evaluate((t) => localStorage.setItem('token', t), token);
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('text=Dashboard', { timeout: 30000 });
});

test('should display namespace list', async ({ page }) => {
// 打开 Namespaces 页面
await page.waitForSelector('text=Namespaces', { timeout: 60000 });
await page.click('text=Namespaces');

// 获取表格元素并验证可见
const table = page.locator('table');
await expect(table).toBeVisible({ timeout: 30000 });

// 验证表格中包含默认 namespace
await expect(table).toContainText('default');

// Debug
if(process.env.DEBUG === 'true'){
await page.screenshot({ path: 'debug-namespace-list.png', fullPage: true });
}
});
Loading
Loading