diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b7dd866..2c186d0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,7 +34,7 @@ permissions: jobs: run-test: name: ${{ matrix.type }} ${{ matrix.flavor }} - runs-on: ubuntu-latest + runs-on: uds-marketplace-ubuntu-big-boy-8-core timeout-minutes: 25 strategy: matrix: @@ -63,3 +63,40 @@ jobs: uses: defenseunicorns/uds-common/.github/actions/save-logs@772b3337950b7c8e0882c527263684306bba7ce4 # v0.7.1 with: suffix: ${{ matrix.type }}-${{ matrix.flavor }}-${{ github.run_id }}-${{ github.run_attempt }} + + - name: Print cluster info + if: always() + shell: bash -e -o pipefail {0} + run: | + kubectl get nodes -o wide + + - name: Print pod info + if: always() + shell: bash -e -o pipefail {0} + run: | + kubectl get pods -A -o wide + + - name: Print service info + if: always() + shell: bash -e -o pipefail {0} + run: | + kubectl get svc -A -o wide + + - name: Print events + if: always() + shell: bash -e -o pipefail {0} + run: | + kubectl get events -A -o wide + + - name: Print Jenkins pod logs + if: always() + shell: bash -e -o pipefail {0} + run: | + kubectl logs jenkins-0 -n jenkins + + - name: Upload screenshot on failure + if: failure() + uses: actions/upload-artifact@v2 + with: + name: playwright-screenshot + path: tests/screenshots/* diff --git a/.gitignore b/.gitignore index 0099811..81bfb03 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ defense-unicorns-distro/preflight.sh .terraform tmp zarf-sbom - +.vscode/ .cache/ .idea/ build/ @@ -26,3 +26,8 @@ test/tf/public-ec2-instance/.terraform terraform.tfstate terraform.tfstate.backup .terraform.lock.hcl + +# Tests +node_modules/ +.playwright/ +screenshots/ diff --git a/README.md b/README.md index af8ff01..21e0f1b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🚚 UDS Jenkins Zarf Package +# 🏪 UDS Jenkins Zarf Package [![Latest Release](https://img.shields.io/github/v/release/defenseunicorns/uds-package-jenkins)](https://github.com/defenseunicorns/uds-package-jenkins/releases) [![Build Status](https://img.shields.io/github/actions/workflow/status/defenseunicorns/uds-package-jenkins/tag-and-release.yaml)](https://github.com/defenseunicorns/uds-package-jenkins/actions/workflows/tag-and-release.yaml) diff --git a/chart/templates/uds-package.yaml b/chart/templates/uds-package.yaml index b79365c..a072be6 100644 --- a/chart/templates/uds-package.yaml +++ b/chart/templates/uds-package.yaml @@ -23,7 +23,7 @@ spec: network: expose: - service: jenkins - podLabels: + selector: app.kubernetes.io/name: jenkins gateway: tenant host: jenkins @@ -36,19 +36,29 @@ spec: remoteGenerated: IntraNamespace - direction: Egress - podLabels: + remoteGenerated: Anywhere + selector: app.kubernetes.io/name: jenkins port: 443 description: "Jenkins-plugins & SSO" - direction: Egress - podLabels: + remoteNamespace: keycloak + remoteSelector: + app.kubernetes.io/name: keycloak + selector: + app.kubernetes.io/name: jenkins + port: 8080 + description: "SSO Internal" + + - direction: Egress + selector: jenkins/label: jenkins-jenkins-agent port: 443 description: "Jenkins-jobs phone home" - direction: Egress - podLabels: + selector: app.kubernetes.io/name: jenkins remoteGenerated: KubeAPI diff --git a/common/zarf.yaml b/common/zarf.yaml index a1b6a99..2aacedd 100644 --- a/common/zarf.yaml +++ b/common/zarf.yaml @@ -4,6 +4,10 @@ metadata: name: jenkins-common description: "UDS jenkins Common Package" +variables: + - name: JENKINS_CLIENT_SECRET + default: "" + components: - name: jenkins-config required: true @@ -22,7 +26,7 @@ components: name: jenkins namespace: jenkins condition: "'{.status.phase}'=Ready" - - cmd: zarf tools kubectl get secrets sso-client-uds-package-jenkins -n jenkins -o=jsonpath='{.data.secret}' | base64 -d + - cmd: ./zarf tools kubectl get secrets sso-client-uds-package-jenkins -n jenkins -o=jsonpath='{.data.secret}' | base64 -d mute: true setVariables: - name: JENKINS_CLIENT_SECRET diff --git a/tasks.yaml b/tasks.yaml index df930e7..3c01932 100644 --- a/tasks.yaml +++ b/tasks.yaml @@ -62,8 +62,10 @@ tasks: - task: create-test-bundle - task: setup:k3d-test-cluster - task: deploy:test-bundle + - task: setup:create-doug-user - task: test:health-check - task: test:ingress + - task: test:ui - name: test-upgrade description: Test an upgrade from the latest released package to the current branch @@ -71,7 +73,9 @@ tasks: - task: create-latest-release-bundle - task: setup:k3d-test-cluster - task: deploy:test-bundle + - task: setup:create-doug-user - task: create-test-bundle - task: deploy:test-bundle - task: test:health-check - task: test:ingress + - task: test:ui diff --git a/tasks/test.yaml b/tasks/test.yaml index 4a5b222..dfe00c3 100644 --- a/tasks/test.yaml +++ b/tasks/test.yaml @@ -16,3 +16,13 @@ tasks: protocol: https address: jenkins.uds.dev/login code: 200 + + - name: ui + description: Jenkins UI Checks + actions: + - cmd: npm ci + dir: tests + - cmd: npx playwright install --with-deps + dir: tests + - cmd: npx playwright test + dir: tests diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts new file mode 100644 index 0000000..fe2742d --- /dev/null +++ b/tests/auth.setup.ts @@ -0,0 +1,43 @@ +import { test as setup, expect } from '@playwright/test'; +import { authFile } from './playwright.config'; +import path from 'path'; + +// Function to generate a unique screenshot filename with a custom base name +const getUniqueScreenshotPath = (baseName: string) => { + let screenshotPath = path.resolve(__dirname, 'screenshots', `${baseName}.png`); + return screenshotPath; +}; + +setup('authenticate', async ({ page, context, baseURL }) => { + console.log('Current working directory:', process.cwd()); + await page.goto(baseURL); + await page.getByLabel("Username or email").fill("doug"); + await page.getByLabel("Password").fill("unicorn123!@#"); + const screenshotPath = getUniqueScreenshotPath('beforeLogin'); + console.log('Screenshot will be saved to:', screenshotPath); + await page.screenshot({ path: screenshotPath }); + await page.getByRole("button", { name: "Log In" }).click(); + + // ensure auth cookies were set + const cookies = await context.cookies(); + const keycloakCookie = cookies.find( + (cookie) => cookie.name === "KEYCLOAK_SESSION", + ); + + expect(keycloakCookie).toBeDefined(); + expect(keycloakCookie?.value).not.toBe(""); + expect(keycloakCookie?.domain).toContain("sso."); + + await page.context().storageState({ path: authFile }); + + try { + await expect(page).toHaveURL(baseURL); + } catch (error) { + console.log('URL assertion failed'); + const currentURL = page.url(); + const screenshotPath = getUniqueScreenshotPath('afterLogin'); + console.log('Screenshot will be saved to:', screenshotPath); + await page.screenshot({ path: screenshotPath }); + throw error; // Rethrow the error after taking the screenshot + } +}) diff --git a/tests/jenkins.test.ts b/tests/jenkins.test.ts new file mode 100644 index 0000000..939fa34 --- /dev/null +++ b/tests/jenkins.test.ts @@ -0,0 +1,114 @@ +import { test, expect, devices } from '@playwright/test'; +import path from 'path'; + +// Function to generate a unique screenshot filename with a custom base name +const getUniqueScreenshotPath = (baseName: string) => { + let screenshotPath = path.resolve(__dirname, 'screenshots', `${defaultBrowserType}-${baseName}.png`); + return screenshotPath; +}; + +let defaultBrowserType: string; + +test.beforeEach(async ({ browserName }) => { + // Use browserName provided by Playwright to determine the browser type + defaultBrowserType = browserName; +}); + +test.describe('Jenkins Pipeline', () => { + const randomSuffix = Math.floor(Math.random() * 10000); // Generate a random number + + test('Should create a simple pipeline and check its status', async ({ page, baseURL }) => { + const pipelineName = `example-pipeline-${defaultBrowserType}-${randomSuffix}`; + // Navigate to Jenkins dashboard + await page.goto(baseURL); + + // Wait for the dashboard to load and verify login by checking for the logout button + await page.waitForSelector('a[href="/logout"]'); + console.log('Logged in successfully using stored auth state'); + + // Retrieve and print cookies + const cookies = await page.context().cookies(); + console.log('Cookies:', cookies); + + // Wait for the "New Item" link to be visible and click it + await page.waitForSelector('a.task-link[href="/view/all/newJob"]', { timeout: 30000 }); + console.log('New Item link is visible'); + + await page.click('a.task-link[href="/view/all/newJob"]'); + console.log('Clicked on New Item link'); + + // Enter pipeline name + await page.waitForTimeout(2000); + await page.fill('input[name="name"]', pipelineName); + console.log(`Entered pipeline name: ${pipelineName}`); + + // Select 'Pipeline' type + await page.click('li.org_jenkinsci_plugins_workflow_job_WorkflowJob'); + + // Verify if 'Pipeline' type is checked (aria-checked="true") + await page.waitForSelector('li.org_jenkinsci_plugins_workflow_job_WorkflowJob.active[aria-checked="true"]', { timeout: 5000 }); + console.log('Pipeline job type is checked'); + var screenshotPath = getUniqueScreenshotPath('job-page'); + await page.screenshot({ path: screenshotPath }); + + // Click OK to create the pipeline + await page.click('button[type="submit"]'); + console.log('Clicked OK to create the pipeline'); + + // Wait for the configuration page to load + await page.waitForSelector('div.jenkins-section__title#pipeline'); + console.log('Configuration page loaded'); + + // Enter a simple pipeline script + const pipelineScript = ` + pipeline { + agent any + stages { + stage('Build') { + steps { + echo 'Building...' + } + } + stage('Test') { + steps { + echo 'Testing...' + } + } + stage('Deploy') { + steps { + echo 'Deploying...' + } + } + } + } + `; + await page.waitForTimeout(2000); + await page.fill('.ace_text-input', pipelineScript); + console.log('Entered pipeline script'); + await page.waitForTimeout(2000); + screenshotPath = getUniqueScreenshotPath('pipeline-page'); + await page.screenshot({ path: screenshotPath }); + + // Save the pipeline + await page.click('button[name="Submit"]'); + console.log('Saved the pipeline configuration'); + + // Run the pipeline + await page.click(`a[href="/job/${pipelineName}/build?delay=0sec"]`); + console.log('Triggered the pipeline build'); + + // Wait for the build to start and complete + await page.waitForTimeout(10000); // Wait for 10 seconds + console.log('Waited for the build to complete'); + + // Check the build status + await page.goto(`/job/${pipelineName}/lastBuild`); + console.log('Navigated to the last build page'); + + // Assert that the build was successful + await page.waitForSelector('svg[tooltip="Success"][title="Success"]', { timeout: 60000 }); + console.log('Build was successful'); + screenshotPath = getUniqueScreenshotPath('results-page'); + await page.screenshot({ path: screenshotPath }); + }); +}); diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 0000000..77651a60 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,104 @@ +{ + "name": "uds-package-mattermost", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "uds-package-mattermost", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.12", + "typescript": "^5.4.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "dev": true, + "dependencies": { + "playwright": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 0000000..4b28f77 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,9 @@ +{ + "name": "uds-package-mattermost", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.12", + "typescript": "^5.4.5" + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts new file mode 100644 index 0000000..8b4e5c3 --- /dev/null +++ b/tests/playwright.config.ts @@ -0,0 +1,44 @@ +import { defineConfig, devices } from '@playwright/test'; + +export const playwrightDir = '.playwright'; +export const authFile = `${playwrightDir}/auth/user.json`; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + timeout: 5 * 60 * 1000, + fullyParallel: true, + forbidOnly: !!process.env.CI, // fail CI if you accidently leave `test.only` in source + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [ + // Reporter to use. See https://playwright.dev/docs/test-reporters + ['html', { outputFolder: `${playwrightDir}/reports`, open: 'never' }], + ['json', { outputFile: `${playwrightDir}/reports/test-results.json`, open: 'never' }], + ['list'] + ], + + outputDir: `${playwrightDir}/output`, + + use: { + baseURL: process.env.BASE_URL || 'https://jenkins.uds.dev', // for `await page.goto('/')` etc + trace: 'on-first-retry', // collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer + }, + + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, // authentication + + ...[ + 'Desktop Chrome', + 'Desktop Firefox', + ].map((p) => ({ + name: devices[p].defaultBrowserType, + dependencies: ['setup'], + use: { + ...devices[p], + storageState: authFile, + }, + })), + ], +}); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..a3b60e8 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/zarf.yaml b/zarf.yaml index 201ad3d..f8386d7 100644 --- a/zarf.yaml +++ b/zarf.yaml @@ -11,8 +11,6 @@ metadata: variables: - name: DOMAIN default: "uds.dev" - - name: JENKINS_CLIENT_SECRET - default: "" components: - name: jenkins-config