Compare commits

..

No commits in common. "main" and "prod-beta" have entirely different histories.

459 changed files with 37130 additions and 72115 deletions

6
.eslintignore Normal file
View file

@ -0,0 +1,6 @@
# Ignore programatically generated API slices
imageBuilderApi.ts
contentSourcesApi.ts
rhsmApi.ts
provisioningApi.ts
edgeApi.ts

4
.eslintrc-typescript.yml Normal file
View file

@ -0,0 +1,4 @@
rules:
"@typescript-eslint/no-unused-vars": "error"
"@typescript-eslint/explicit-module-boundary-types": "off"

42
.eslintrc.yml Normal file
View file

@ -0,0 +1,42 @@
extends: [
"@redhat-cloud-services/eslint-config-redhat-cloud-services",
"react-app",
"react-app/jest"
]
globals:
insights: 'readonly'
shallow: readonly
render: 'readonly'
mount: 'readonly'
plugins:
- import
rules:
rulesdir/forbid-pf-relative-imports: off
import/order:
- error
- groups:
- builtin
- external
- internal
- sibling
- parent
- index
alphabetize:
order: asc
caseInsensitive: true
newlines-between: always
pathGroups: # ensures the import of React is always on top
- pattern: react
group: builtin
position: before
pathGroupsExcludedImportTypes:
- react
prefer-const:
- error
- destructuring: any
no-console: 2
eqeqeq: error
overrides:
- files: "**/*.ts?(x)"
parser: "@typescript-eslint/parser"
extends: ".eslintrc-typescript.yml"

View file

@ -1 +0,0 @@
1

View file

@ -1,257 +0,0 @@
---
name: Debian Image Builder Frontend CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: "18"
DEBIAN_FRONTEND: noninteractive
jobs:
build-and-test:
name: Build and Test Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: |
npm ci
npm run build || echo "Build script not found"
- name: Run tests
run: |
if [ -f package.json ] && npm run test; then
npm test
else
echo "No test script found, skipping tests"
fi
- name: Run linting
run: |
if [ -f package.json ] && npm run lint; then
npm run lint
else
echo "No lint script found, skipping linting"
fi
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
dist/
build/
retention-days: 30
package:
name: Package Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
devscripts \
debhelper \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: npm ci
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Create debian directory
run: |
mkdir -p debian
cat > debian/control << EOF
Source: debian-image-builder-frontend
Section: web
Priority: optional
Maintainer: Debian Forge Team <team@debian-forge.org>
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
Standards-Version: 4.6.2
Package: debian-image-builder-frontend
Architecture: all
Depends: \${misc:Depends}, nodejs, nginx
Description: Debian Image Builder Frontend
Web-based frontend for Debian Image Builder with Cockpit integration.
Provides a user interface for managing image builds, blueprints,
and system configurations through a modern React application.
EOF
cat > debian/rules << EOF
#!/usr/bin/make -f
%:
dh \$@
override_dh_auto_install:
dh_auto_install
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
# Copy built frontend files
if [ -d dist ]; then
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
elif [ -d build ]; then
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
fi
# Copy source files for development
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
# Create nginx configuration
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
server {
listen 80;
server_name localhost;
root /usr/share/debian-image-builder-frontend;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
NGINX_EOF
# Create cockpit manifest
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
{
"version": 1,
"manifest": {
"name": "debian-image-builder",
"version": "1.0.0",
"title": "Debian Image Builder",
"description": "Build and manage Debian atomic images",
"url": "/usr/share/debian-image-builder-frontend",
"icon": "debian-logo",
"requires": {
"cockpit": ">= 200"
}
}
}
COCKPIT_EOF
EOF
cat > debian/changelog << EOF
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
* Initial release
* Debian Image Builder Frontend with Cockpit integration
* React-based web interface for image management
-- Debian Forge Team <team@debian-forge.org> $(date -R)
EOF
cat > debian/compat << EOF
13
EOF
chmod +x debian/rules
- name: Build Debian package
run: |
dpkg-buildpackage -us -uc -b
ls -la ../*.deb
- name: Upload Debian package
uses: actions/upload-artifact@v4
with:
name: debian-image-builder-frontend-deb
path: ../*.deb
retention-days: 30
cockpit-integration:
name: Test Cockpit Integration
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install Node.js dependencies
run: npm ci
- name: Test cockpit integration
run: |
echo "Testing Cockpit integration..."
if [ -d cockpit ]; then
echo "Cockpit directory found:"
ls -la cockpit/
else
echo "No cockpit directory found"
fi
if [ -f package.json ]; then
echo "Package.json scripts:"
npm run
fi

View file

@ -1,257 +0,0 @@
---
name: Debian Image Builder Frontend CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
env:
NODE_VERSION: "18"
DEBIAN_FRONTEND: noninteractive
jobs:
build-and-test:
name: Build and Test Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: |
npm ci
npm run build || echo "Build script not found"
- name: Run tests
run: |
if [ -f package.json ] && npm run test; then
npm test
else
echo "No test script found, skipping tests"
fi
- name: Run linting
run: |
if [ -f package.json ] && npm run lint; then
npm run lint
else
echo "No lint script found, skipping linting"
fi
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
dist/
build/
retention-days: 30
package:
name: Package Frontend
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install build dependencies
run: |
apt-get update
apt-get install -y \
build-essential \
devscripts \
debhelper \
git \
ca-certificates \
python3
- name: Install Node.js dependencies
run: npm ci
- name: Build production bundle
run: |
if [ -f package.json ] && npm run build; then
npm run build
else
echo "No build script found"
fi
- name: Create debian directory
run: |
mkdir -p debian
cat > debian/control << EOF
Source: debian-image-builder-frontend
Section: web
Priority: optional
Maintainer: Debian Forge Team <team@debian-forge.org>
Build-Depends: debhelper (>= 13), nodejs, npm, git, ca-certificates
Standards-Version: 4.6.2
Package: debian-image-builder-frontend
Architecture: all
Depends: \${misc:Depends}, nodejs, nginx
Description: Debian Image Builder Frontend
Web-based frontend for Debian Image Builder with Cockpit integration.
Provides a user interface for managing image builds, blueprints,
and system configurations through a modern React application.
EOF
cat > debian/rules << EOF
#!/usr/bin/make -f
%:
dh \$@
override_dh_auto_install:
dh_auto_install
mkdir -p debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend
mkdir -p debian/debian-image-builder-frontend/etc/nginx/sites-available
mkdir -p debian/debian-image-builder-frontend/etc/cockpit
# Copy built frontend files
if [ -d dist ]; then
cp -r dist/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
elif [ -d build ]; then
cp -r build/* debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
fi
# Copy source files for development
cp -r src debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
cp package.json debian/debian-image-builder-frontend/usr/share/debian-image-builder-frontend/
# Create nginx configuration
cat > debian/debian-image-builder-frontend/etc/nginx/sites-available/debian-image-builder-frontend << 'NGINX_EOF'
server {
listen 80;
server_name localhost;
root /usr/share/debian-image-builder-frontend;
index index.html;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
NGINX_EOF
# Create cockpit manifest
cat > debian/debian-image-builder-frontend/etc/cockpit/debian-image-builder.manifest << 'COCKPIT_EOF'
{
"version": 1,
"manifest": {
"name": "debian-image-builder",
"version": "1.0.0",
"title": "Debian Image Builder",
"description": "Build and manage Debian atomic images",
"url": "/usr/share/debian-image-builder-frontend",
"icon": "debian-logo",
"requires": {
"cockpit": ">= 200"
}
}
}
COCKPIT_EOF
EOF
cat > debian/changelog << EOF
debian-image-builder-frontend (1.0.0-1) unstable; urgency=medium
* Initial release
* Debian Image Builder Frontend with Cockpit integration
* React-based web interface for image management
-- Debian Forge Team <team@debian-forge.org> $(date -R)
EOF
cat > debian/compat << EOF
13
EOF
chmod +x debian/rules
- name: Build Debian package
run: |
dpkg-buildpackage -us -uc -b
ls -la ../*.deb
- name: Upload Debian package
uses: actions/upload-artifact@v4
with:
name: debian-image-builder-frontend-deb
path: ../*.deb
retention-days: 30
cockpit-integration:
name: Test Cockpit Integration
runs-on: ubuntu-latest
container:
image: node:18-bullseye
needs: build-and-test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js environment
run: |
node --version
npm --version
- name: Install Node.js dependencies
run: npm ci
- name: Test cockpit integration
run: |
echo "Testing Cockpit integration..."
if [ -d cockpit ]; then
echo "Cockpit directory found:"
ls -la cockpit/
else
echo "No cockpit directory found"
fi
if [ -f package.json ]; then
echo "Package.json scripts:"
npm run
fi

View file

@ -7,9 +7,3 @@ updates:
time: "04:00"
open-pull-requests-limit: 3
rebase-strategy: "auto"
ignore:
- dependency-name: "@playwright/test"
update-types:
- "version-update:semver-major"
- "version-update:semver-minor"
- "version-update:semver-patch"

View file

@ -1,24 +0,0 @@
# This action creates a release every second Wednesday
name: "Create and push release tag"
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * 3"
jobs:
tag-and-push:
runs-on: ubuntu-latest
steps:
- name: Even or odd week
run: if [ `expr \`date +\%s\` / 86400 \% 2` -eq 0 ]; then echo "WEEK=odd" >> $GITHUB_ENV; else echo "WEEK=even" >> $GITHUB_ENV; fi
shell: bash
- name: Upstream tag
uses: osbuild/release-action@create-tag
if: ${{ env.WEEK == 'odd' || github.event_name != 'schedule' }}
with:
token: "${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }}"
username: "imagebuilder-bot"
email: "imagebuilder-bots+imagebuilder-bot@redhat.com"

View file

@ -1,83 +0,0 @@
name: Development checks
on:
pull_request:
branches: [ "main" ]
push:
branches: [ "main" ]
merge_group:
concurrency:
group: ${{github.workflow}}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run build
run: npm run build
lint-checks:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run lint check
run: npm run lint
circular-dependencies:
name: Circular Dependencies Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for circular dependencies
run: npm run circular
api-changes:
name: Manual API Changes Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for manual changes to API
run: |
npm run api
if [ -n "$(git status --porcelain)" ]; then
echo
echo "✗ API manually changed, please refer to the README for the procedure to follow for programmatically generated API endpoints."
exit 1
else
echo
echo "✓ No manual API changes."
exit 0
fi

View file

@ -1,90 +0,0 @@
name: Hosted playwright tests
on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled]
workflow_dispatch:
merge_group:
# this prevents multiple jobs from the same pr
# running when new changes are pushed.
concurrency:
group: ${{github.workflow}}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright-tests:
runs-on:
- codebuild-image-builder-frontend-${{ github.run_id }}-${{ github.run_attempt }}
- instance-size:large
- buildspec-override:true
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get current PR URL
id: get-pr-url
run: |
# Extract the pull request URL from the event payload
pr_url=$(jq -r '.pull_request.html_url' < "$GITHUB_EVENT_PATH")
echo "Pull Request URL: $pr_url"
# Set the PR URL as an output using the environment file
echo "pr_url=$pr_url" >> $GITHUB_ENV
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
- name: Install front-end dependencies
run: npm ci
- name: Install playwright
run: npx playwright install --with-deps
# This prevents an error related to minimum watchers when running the front-end and playwright
- name: Increase file watchers limit
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- name: Update /etc/hosts
run: sudo npm run patch:hosts
- name: Start front-end server
run: |
npm run start:federated &
npx wait-on http://localhost:8003/apps/image-builder/
- name: Run testing proxy
run: docker run -d --network=host -e HTTPS_PROXY=$RH_PROXY_URL -v "$(pwd)/config:/config:ro,Z" --name consoledot-testing-proxy quay.io/dvagner/consoledot-testing-proxy
- name: Run front-end Playwright tests
env:
BASE_URL: https://stage.foo.redhat.com:1337
run: |
export PLAYWRIGHT_USER=image-builder-playwright-$RANDOM
export PLAYWRIGHT_PASSWORD=image-builder-playwright-$(uuidgen)
# Step 1: Create a new empty account
curl -k -X POST https://account-manager-stage.app.eng.rdu2.redhat.com/account/new -d "{\"username\": \"$PLAYWRIGHT_USER\", \"password\":\"$PLAYWRIGHT_PASSWORD\"}"
# Step 2: Attach subscriptions to the new account
curl -k -X POST https://account-manager-stage.app.eng.rdu2.redhat.com/account/attach \
-d "{\"username\": \"$PLAYWRIGHT_USER\", \"password\":\"$PLAYWRIGHT_PASSWORD\", \"sku\":[\"RH00003\"],\"quantity\": 1}"
# Step 3: Activate the new account by accepting Terms and Conditions
curl -k -X POST https://account-manager-stage.app.eng.rdu2.redhat.com/account/activate -d "{\"username\": \"$PLAYWRIGHT_USER\", \"password\":\"$PLAYWRIGHT_PASSWORD\"}"
# Step 4: Refresh account to update subscription pools
curl -k -X POST https://account-manager-stage.app.eng.rdu2.redhat.com/account/refresh -d "{\"username\": \"$PLAYWRIGHT_USER\", \"password\":\"$PLAYWRIGHT_PASSWORD\"}"
# Step 5: View account to check account status
curl -k -X GET "https://account-manager-stage.app.eng.rdu2.redhat.com/account/get?username=$PLAYWRIGHT_USER&password=$PLAYWRIGHT_PASSWORD"
CURRENTS_PROJECT_ID=hIU6nO CURRENTS_RECORD_KEY=$CURRENTS_RECORD_KEY npx playwright test
- name: Store front-end Test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 10

View file

@ -1,19 +0,0 @@
name: "Verify PR best practices"
on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened, edited]
issue_comment:
types: [created]
merge_group:
jobs:
pr-best-practices:
runs-on: ubuntu-latest
steps:
- name: PR best practice check
uses: osbuild/pr-best-practices@main
with:
token: ${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }}
jira_token: ${{ secrets.IMAGEBUILDER_BOT_JIRA_TOKEN }}

View file

@ -1,47 +0,0 @@
name: "Create GitHub release"
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
# create release artefact before creating the release to get the correct release in the
# artefact name.
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Make dist
run: |
make dist
RELEASE_NO=$(echo ${{github.ref_name}} | tr -d 'v')
mv "cockpit-image-builder-$RELEASE_NO.tar.gz" ../cockpit-image-builder-$RELEASE_NO.tar.gz
# create release, which will bump the version
- name: Upstream release
uses: osbuild/release-action@main
with:
token: "${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }}"
slack_webhook_url: "${{ secrets.SLACK_WEBHOOK_URL }}"
# upload release artefact
# Source0 expands to `https://github.com/osbuild/image-builder-frontend/releases/download/v$VERSION/cockpit-image-builder-v$VERSION.tar.gz`,
# so the v needs to be in the tarball when we upload it as a release artefact.
- name: Upload release artefact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_NO=$(echo ${{github.ref_name}} | tr -d 'v')
gh release upload ${{github.ref_name}} \
../cockpit-image-builder-$RELEASE_NO.tar.gz

View file

@ -1,33 +0,0 @@
name: sentryInit
on:
push:
branches:
- master
workflow_dispatch:
inputs:
commit_hash:
description: 'The commit hash (or branch/tag) to build'
required: false
default: ''
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.commit_hash || 'refs/heads/master' }}
- name: Install dependencies
run: npm ci
- name: Build
env:
ENABLE_SENTRY: ${{ secrets.ENABLE_SENTRY }}
SENTRY_RELEASE: ${{ github.event.inputs.commit_hash && github.event.inputs.commit_hash }}
SENTRY_AUTH_TOKEN: ${{ github.event.inputs.commit_hash && secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
run: npm run build --if-present

View file

@ -8,7 +8,6 @@ jobs:
stale:
runs-on: ubuntu-latest
permissions:
actions: write # needed to clean up the saved action state
issues: write
pull-requests: write
steps:

79
.github/workflows/sync-branches.yml vendored Normal file
View file

@ -0,0 +1,79 @@
name: Sync branches
on:
push:
branches:
- main
workflow_dispatch:
inputs:
source:
description: Source ref (branch or sha)
required: true
type: string
target:
description: Target branch
required: true
type: choice
options:
- production
- stage-stable
jobs:
check:
name: Validate source and target refs
timeout-minutes: 1
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.source }}
fetch-depth: 0
- name: Check ancestry
if: ${{ github.event.inputs.target == 'production' }}
run: |
if ! git merge-base --is-ancestor ${{ github.event.inputs.source }} origin/main; then
echo "Target is production and source ref isn't an ancestor of main"
exit 1
fi
if ! git merge-base --is-ancestor ${{ github.event.inputs.source }} origin/stage-stable; then
echo "Target is production and source ref isn't deployed in stage-stable"
echo "The main and stage-stable branches should be in sync, please fix"
exit 1
fi
- name: Check stage-stable manual sync
if: ${{ github.event.inputs.target == 'stage-stable' }}
run: |
if [ $(git rev-parse ${{ github.event.inputs.source }}) != $(git rev-parse origin/main) ]; then
echo "Target is stage-stable and source ref isn't main"
exit 1
fi
sync:
name: Sync source and target refs
needs: check
if: ${{ github.event_name == 'push' || success() }}
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.source }}
fetch-depth: 0
- name: Release to production
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'production' }}
run: |
git push https://${{ secrets.SCHUTZBOT_GH_TOKEN }}@github.com/${{ github.repository }}.git ${{ github.event.inputs.source }}:refs/heads/prod-beta
git push https://${{ secrets.SCHUTZBOT_GH_TOKEN }}@github.com/${{ github.repository }}.git ${{ github.event.inputs.source }}:refs/heads/prod-stable
- name: Sync main to stage-stable
if: ${{ github.event_name == 'push' || github.event.inputs.target == 'stage-stable' }}
run: |
git push https://${{ secrets.SCHUTZBOT_GH_TOKEN }}@github.com/${{ github.repository }}.git origin/main:refs/heads/stage-stable

View file

@ -3,58 +3,25 @@
name: Trigger GitLab CI
on:
workflow_run:
workflows: ["Development checks"]
types: [completed]
push:
branches:
- main
jobs:
trigger-gitlab:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
env:
IMAGEBUILDER_BOT_GITLAB_SSH_KEY: ${{ secrets.IMAGEBUILDER_BOT_GITLAB_SSH_KEY }}
GITLAB_TOKEN: ${{ secrets.IMAGEBUILDER_BOT_GITLAB_PIPELINE_TRIGGER_TOKEN }}
steps:
- name: Report status
uses: haya14busa/action-workflow_run-status@v1
- name: Install Dependencies
run: |
sudo apt install -y jq
- name: Clone repository
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- uses: octokit/request-action@v2.x
id: fetch_pulls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
route: GET /repos/${{ github.repository }}/pulls
per_page: 100
- name: Checkout branch
id: pr_data
env:
BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
PR_DATA=$(mktemp)
# use uuid as a file terminator to avoid conflicts with data content
cat > "$PR_DATA" <<'a21b3e7f-d5eb-44a3-8be0-c2412851d2e6'
${{ steps.fetch_pulls.outputs.data }}
a21b3e7f-d5eb-44a3-8be0-c2412851d2e6
PR=$(jq -rc '.[] | select(.head.sha | contains("${{ github.event.workflow_run.head_sha }}")) | select(.state | contains("open"))' "$PR_DATA" | jq -r .number)
if [ ! -z "$PR" ]; then
echo "pr_branch=PR-$PR" >> "$GITHUB_OUTPUT"
git checkout -b PR-$PR
else
git checkout "${BRANCH}"
fi
- name: Push to gitlab
run: |
mkdir -p ~/.ssh
@ -63,5 +30,4 @@ jobs:
touch ~/.ssh/known_hosts
ssh-keyscan -t rsa gitlab.com >> ~/.ssh/known_hosts
git remote add ci git@gitlab.com:redhat/services/products/image-builder/ci/image-builder-frontend.git
[[ "${SKIP_CI}" == true ]] && PUSH_OPTION='-o ci.variable="SKIP_CI=true"' || PUSH_OPTION=""
git push -f ${PUSH_OPTION} ci
git push -f ci

View file

@ -1,51 +0,0 @@
name: Unit Tests
on:
pull_request:
branches: [ "main" ]
push:
branches: [ "main" ]
merge_group:
# this prevents multiple jobs from the same pr
# running when new changes are pushed.
concurrency:
group: ${{github.workflow}}-${{ github.ref }}
cancel-in-progress: true
jobs:
unit-tests:
name: Service Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/junit.xml
verbose: true
cockpit-unit-tests:
name: Cockpit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests with cockpit
run: npm run test:cockpit

View file

@ -1,51 +0,0 @@
# This action checks API updates every day at 5:00 UTC.
name: Update API code generation
on:
workflow_dispatch:
schedule:
- cron: "0 5 * * *"
jobs:
update-api:
name: "Update API definitions"
if: github.repository == 'osbuild/image-builder-frontend'
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Mark the working directory as safe for git
run: git config --global --add safe.directory "$(pwd)"
- name: Run API code generation
run: npm run api
- name: Check if there are any changes
run: |
if [ "$(git status --porcelain)" ]; then
echo
echo "API codegen is up-to-date"
exit "0"
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
branch: update-apis
delete-branch: true
title: "api: regenerate api code generation"
commit-message: "api: regenerate api code generation"
body: Update api code generation
token: ${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }}
author: schutzbot <schutzbot@gmail.com>

28
.gitignore vendored
View file

@ -12,9 +12,6 @@ node_modules
# production
dist
# cache
.cache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@ -23,29 +20,4 @@ yarn-error.log*
coverage
*~
*.swp
bots
# madge graph of dependencies generated by `npm run circular:graph`
deps.png
# build directories
cockpit/public/vendor*
cockpit/public/src_*
cockpit/public/main*
# Sentry Config File
.sentryclirc
# cockpit lib dir
pkg/lib
rpmbuild
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.env
.auth

View file

@ -3,41 +3,31 @@ stages:
- test
- finish
.terraform:
after_script:
- schutzbot/update_github_status.sh update
tags:
- terraform
init:
stage: init
interruptible: true
tags:
- shell
script:
- schutzbot/update_github_status.sh start
test:
before_script:
- mkdir -p /tmp/artifacts
- schutzbot/ci_details.sh > /tmp/artifacts/ci-details-before-run.txt
- cat schutzbot/team_ssh_keys.txt | tee -a ~/.ssh/authorized_keys > /dev/null
SonarQube:
stage: test
extends: .terraform
script:
- schutzbot/make_rpm_and_install.sh
- schutzbot/playwright_tests.sh
after_script:
- schutzbot/ci_details.sh > /tmp/artifacts/ci-details-after-run.txt || true
- schutzbot/unregister.sh || true
- schutzbot/update_github_status.sh update || true
- schutzbot/save_journal.sh || true
- schutzbot/upload_artifacts.sh
tags:
- terraform
parallel:
matrix:
- RUNNER:
- aws/fedora-41-x86_64
- aws/fedora-42-x86_64
- aws/rhel-10.1-nightly-x86_64
INTERNAL_NETWORK: ["true"]
- schutzbot/sonarqube.sh
variables:
RUNNER: aws/centos-stream-8-x86_64
INTERNAL_NETWORK: "true"
GIT_DEPTH: 0
finish:
stage: finish
dependencies: []
tags:
- shell
script:

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "build-tools"]
path = build-tools
url = https://github.com/RedHatInsights/insights-frontend-builder-common.git

View file

@ -1,3 +0,0 @@
name = "basic-example"
description = "A basic blueprint"
version = "0.0.1"

View file

@ -1,7 +0,0 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

4
.stylelintrc.json Normal file
View file

@ -0,0 +1,4 @@
{
"extends": "stylelint-config-recommended-scss",
"customSyntax": "postcss-scss"
}

View file

@ -1,558 +0,0 @@
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
annotations:
build.appstudio.openshift.io/repo: https://github.com/osbuild/image-builder-frontend?rev={{revision}}
build.appstudio.redhat.com/commit_sha: '{{revision}}'
build.appstudio.redhat.com/pull_request_number: '{{pull_request_number}}'
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
pipelinesascode.tekton.dev/max-keep-runs: "3"
pipelinesascode.tekton.dev/on-cel-expression: (event == "pull_request" && target_branch == "main") || (event == "push" && target_branch.startsWith("gh-readonly-queue/main/"))
creationTimestamp:
labels:
appstudio.openshift.io/application: insights-image-builder
appstudio.openshift.io/component: image-builder-frontend
pipelines.appstudio.openshift.io/type: build
name: image-builder-frontend-on-pull-request
namespace: insights-management-tenant
spec:
params:
- name: git-url
value: '{{source_url}}'
- name: revision
value: '{{revision}}'
- name: output-image
value: quay.io/redhat-user-workloads/insights-management-tenant/insights-image-builder/image-builder-frontend:on-pr-{{revision}}
- name: image-expires-after
value: 5d
- name: dockerfile
value: build-tools/Dockerfile
- name: path-context
value: .
pipelineSpec:
description: |
This pipeline is ideal for building container images from a Containerfile while reducing network traffic.
_Uses `buildah` to create a container image. It also optionally creates a source image and runs some build-time tests. EC will flag a violation for [`trusted_task.trusted`](https://enterprisecontract.dev/docs/ec-policies/release_policy.html#trusted_task__trusted) if any tasks are added to the pipeline.
This pipeline is pushed as a Tekton bundle to [quay.io](https://quay.io/repository/konflux-ci/tekton-catalog/pipeline-docker-build?tab=tags)_
finally:
- name: show-sbom
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
taskRef:
params:
- name: name
value: show-sbom
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
- name: kind
value: task
resolver: bundles
- name: show-summary
params:
- name: pipelinerun-name
value: $(context.pipelineRun.name)
- name: git-url
value: $(tasks.clone-repository.results.url)?rev=$(tasks.clone-repository.results.commit)
- name: image-url
value: $(params.output-image)
- name: build-task-status
value: $(tasks.build-image-index.status)
taskRef:
params:
- name: name
value: summary
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
- name: kind
value: task
resolver: bundles
workspaces:
- name: workspace
workspace: workspace
params:
- description: Source Repository URL
name: git-url
type: string
- default: ""
description: Revision of the Source Repository
name: revision
type: string
- description: Fully Qualified Output Image
name: output-image
type: string
- default: .
description: Path to the source code of an application's component from where to build image.
name: path-context
type: string
- default: Dockerfile
description: Path to the Dockerfile inside the context specified by parameter path-context
name: dockerfile
type: string
- default: "false"
description: Force rebuild image
name: rebuild
type: string
- default: "false"
description: Skip checks against built image
name: skip-checks
type: string
- default: "false"
description: Execute the build with network isolation
name: hermetic
type: string
- default: ""
description: Build dependencies to be prefetched by Cachi2
name: prefetch-input
type: string
- default: ""
description: Image tag expiration time, time values could be something like 1h, 2d, 3w for hours, days, and weeks, respectively.
name: image-expires-after
- default: "false"
description: Build a source image.
name: build-source-image
type: string
- default: "false"
description: Add built image into an OCI image index
name: build-image-index
type: string
- default: []
description: Array of --build-arg values ("arg=value" strings) for buildah
name: build-args
type: array
- default: ""
description: Path to a file with build arguments for buildah, see https://www.mankier.com/1/buildah-build#--build-arg-file
name: build-args-file
type: string
results:
- description: ""
name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- description: ""
name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- description: ""
name: CHAINS-GIT_URL
value: $(tasks.clone-repository.results.url)
- description: ""
name: CHAINS-GIT_COMMIT
value: $(tasks.clone-repository.results.commit)
tasks:
- name: init
params:
- name: image-url
value: $(params.output-image)
- name: rebuild
value: $(params.rebuild)
- name: skip-checks
value: $(params.skip-checks)
taskRef:
params:
- name: name
value: init
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
- name: kind
value: task
resolver: bundles
- name: clone-repository
params:
- name: url
value: $(params.git-url)
- name: revision
value: $(params.revision)
runAfter:
- init
taskRef:
params:
- name: name
value: git-clone
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
workspaces:
- name: output
workspace: workspace
- name: basic-auth
workspace: git-auth
- name: prefetch-dependencies
params:
- name: input
value: $(params.prefetch-input)
runAfter:
- clone-repository
taskRef:
params:
- name: name
value: prefetch-dependencies
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
- name: kind
value: task
resolver: bundles
when:
- input: $(params.prefetch-input)
operator: notin
values:
- ""
workspaces:
- name: source
workspace: workspace
- name: git-basic-auth
workspace: git-auth
- name: netrc
workspace: netrc
- name: build-container
params:
- name: IMAGE
value: $(params.output-image)
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
- name: HERMETIC
value: $(params.hermetic)
- name: PREFETCH_INPUT
value: $(params.prefetch-input)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: BUILD_ARGS
value:
- $(params.build-args[*])
- name: BUILD_ARGS_FILE
value: $(params.build-args-file)
runAfter:
- prefetch-dependencies
taskRef:
params:
- name: name
value: buildah
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
workspaces:
- name: source
workspace: workspace
- name: build-image-index
params:
- name: IMAGE
value: $(params.output-image)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: ALWAYS_BUILD_INDEX
value: $(params.build-image-index)
- name: IMAGES
value:
- $(tasks.build-container.results.IMAGE_URL)@$(tasks.build-container.results.IMAGE_DIGEST)
runAfter:
- build-container
taskRef:
params:
- name: name
value: build-image-index
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
- name: build-source-image
params:
- name: BINARY_IMAGE
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: BINARY_IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: source-build
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
- input: $(params.build-source-image)
operator: in
values:
- "true"
workspaces:
- name: workspace
workspace: workspace
- name: sast-shell-check
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-shell-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: sast-unicode-check
params:
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-unicode-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: deprecated-base-image-check
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: deprecated-image-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: clair-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: clair-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: ecosystem-cert-preflight-checks
params:
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: ecosystem-cert-preflight-checks
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: sast-snyk-check
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-snyk-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: clamav-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: clamav-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: apply-tags
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: apply-tags
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
- name: kind
value: task
resolver: bundles
- name: push-dockerfile
params:
- name: IMAGE
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: push-dockerfile
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
- name: kind
value: task
resolver: bundles
workspaces:
- name: workspace
workspace: workspace
- name: rpms-signature-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: rpms-signature-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
- name: git-auth
optional: true
- name: netrc
optional: true
taskRunTemplate:
serviceAccountName: build-pipeline-image-builder-frontend
workspaces:
- name: workspace
volumeClaimTemplate:
metadata:
creationTimestamp:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
status: {}
- name: git-auth
secret:
secretName: '{{ git_auth_secret }}'
status: {}

View file

@ -1,555 +0,0 @@
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
annotations:
build.appstudio.openshift.io/repo: https://github.com/osbuild/image-builder-frontend?rev={{revision}}
build.appstudio.redhat.com/commit_sha: '{{revision}}'
build.appstudio.redhat.com/target_branch: '{{target_branch}}'
pipelinesascode.tekton.dev/max-keep-runs: "3"
pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main"
creationTimestamp:
labels:
appstudio.openshift.io/application: insights-image-builder
appstudio.openshift.io/component: image-builder-frontend
pipelines.appstudio.openshift.io/type: build
name: image-builder-frontend-on-push
namespace: insights-management-tenant
spec:
params:
- name: git-url
value: '{{source_url}}'
- name: revision
value: '{{revision}}'
- name: output-image
value: quay.io/redhat-user-workloads/insights-management-tenant/insights-image-builder/image-builder-frontend:{{revision}}
- name: dockerfile
value: build-tools/Dockerfile
- name: path-context
value: .
pipelineSpec:
description: |
This pipeline is ideal for building container images from a Containerfile while reducing network traffic.
_Uses `buildah` to create a container image. It also optionally creates a source image and runs some build-time tests. EC will flag a violation for [`trusted_task.trusted`](https://enterprisecontract.dev/docs/ec-policies/release_policy.html#trusted_task__trusted) if any tasks are added to the pipeline.
This pipeline is pushed as a Tekton bundle to [quay.io](https://quay.io/repository/konflux-ci/tekton-catalog/pipeline-docker-build?tab=tags)_
finally:
- name: show-sbom
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
taskRef:
params:
- name: name
value: show-sbom
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7
- name: kind
value: task
resolver: bundles
- name: show-summary
params:
- name: pipelinerun-name
value: $(context.pipelineRun.name)
- name: git-url
value: $(tasks.clone-repository.results.url)?rev=$(tasks.clone-repository.results.commit)
- name: image-url
value: $(params.output-image)
- name: build-task-status
value: $(tasks.build-image-index.status)
taskRef:
params:
- name: name
value: summary
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091
- name: kind
value: task
resolver: bundles
workspaces:
- name: workspace
workspace: workspace
params:
- description: Source Repository URL
name: git-url
type: string
- default: ""
description: Revision of the Source Repository
name: revision
type: string
- description: Fully Qualified Output Image
name: output-image
type: string
- default: .
description: Path to the source code of an application's component from where to build image.
name: path-context
type: string
- default: Dockerfile
description: Path to the Dockerfile inside the context specified by parameter path-context
name: dockerfile
type: string
- default: "false"
description: Force rebuild image
name: rebuild
type: string
- default: "false"
description: Skip checks against built image
name: skip-checks
type: string
- default: "false"
description: Execute the build with network isolation
name: hermetic
type: string
- default: ""
description: Build dependencies to be prefetched by Cachi2
name: prefetch-input
type: string
- default: ""
description: Image tag expiration time, time values could be something like 1h, 2d, 3w for hours, days, and weeks, respectively.
name: image-expires-after
- default: "false"
description: Build a source image.
name: build-source-image
type: string
- default: "false"
description: Add built image into an OCI image index
name: build-image-index
type: string
- default: []
description: Array of --build-arg values ("arg=value" strings) for buildah
name: build-args
type: array
- default: ""
description: Path to a file with build arguments for buildah, see https://www.mankier.com/1/buildah-build#--build-arg-file
name: build-args-file
type: string
results:
- description: ""
name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- description: ""
name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- description: ""
name: CHAINS-GIT_URL
value: $(tasks.clone-repository.results.url)
- description: ""
name: CHAINS-GIT_COMMIT
value: $(tasks.clone-repository.results.commit)
tasks:
- name: init
params:
- name: image-url
value: $(params.output-image)
- name: rebuild
value: $(params.rebuild)
- name: skip-checks
value: $(params.skip-checks)
taskRef:
params:
- name: name
value: init
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:08e18a4dc5f947c1d20e8353a19d013144bea87b72f67236b165dd4778523951
- name: kind
value: task
resolver: bundles
- name: clone-repository
params:
- name: url
value: $(params.git-url)
- name: revision
value: $(params.revision)
runAfter:
- init
taskRef:
params:
- name: name
value: git-clone
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:7939000e2f92fc8b5d2c4ee4ba9000433c5aa7700d2915a1d4763853d5fd1fd4
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
workspaces:
- name: output
workspace: workspace
- name: basic-auth
workspace: git-auth
- name: prefetch-dependencies
params:
- name: input
value: $(params.prefetch-input)
runAfter:
- clone-repository
taskRef:
params:
- name: name
value: prefetch-dependencies
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:ce5f2485d759221444357fe38276be876fc54531651e50dcfc0f84b34909d760
- name: kind
value: task
resolver: bundles
when:
- input: $(params.prefetch-input)
operator: notin
values:
- ""
workspaces:
- name: source
workspace: workspace
- name: git-basic-auth
workspace: git-auth
- name: netrc
workspace: netrc
- name: build-container
params:
- name: IMAGE
value: $(params.output-image)
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
- name: HERMETIC
value: $(params.hermetic)
- name: PREFETCH_INPUT
value: $(params.prefetch-input)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: BUILD_ARGS
value:
- $(params.build-args[*])
- name: BUILD_ARGS_FILE
value: $(params.build-args-file)
runAfter:
- prefetch-dependencies
taskRef:
params:
- name: name
value: buildah
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.4@sha256:7782cb7462130de8e8839a58dd15ed78e50938d718b51375267679c6044b4367
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
workspaces:
- name: source
workspace: workspace
- name: build-image-index
params:
- name: IMAGE
value: $(params.output-image)
- name: COMMIT_SHA
value: $(tasks.clone-repository.results.commit)
- name: IMAGE_EXPIRES_AFTER
value: $(params.image-expires-after)
- name: ALWAYS_BUILD_INDEX
value: $(params.build-image-index)
- name: IMAGES
value:
- $(tasks.build-container.results.IMAGE_URL)@$(tasks.build-container.results.IMAGE_DIGEST)
runAfter:
- build-container
taskRef:
params:
- name: name
value: build-image-index
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-build-image-index:0.1@sha256:72f77a8c62f9d6f69ab5c35170839e4b190026e6cc3d7d4ceafa7033fc30ad7b
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
- name: build-source-image
params:
- name: BINARY_IMAGE
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: BINARY_IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: source-build
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-source-build:0.3@sha256:96ed9431854ecf9805407dca77b063abdf7aba1b3b9d1925a5c6145c6b7e95fd
- name: kind
value: task
resolver: bundles
when:
- input: $(tasks.init.results.build)
operator: in
values:
- "true"
- input: $(params.build-source-image)
operator: in
values:
- "true"
workspaces:
- name: workspace
workspace: workspace
- name: sast-shell-check
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-shell-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-shell-check:0.1@sha256:4a63982791a1a68f560c486f524ef5b9fdbeee0c16fe079eee3181a2cfd1c1bf
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: sast-unicode-check
params:
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-unicode-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-unicode-check:0.3@sha256:bec18fa5e82e801c3f267f29bf94535a5024e72476f2b27cca7271d506abb5ad
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: deprecated-base-image-check
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: deprecated-image-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:1d07d16810c26713f3d875083924d93697900147364360587ccb5a63f2c31012
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: clair-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: clair-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.2@sha256:893ffa3ce26b061e21bb4d8db9ef7ed4ddd4044fe7aa5451ef391034da3ff759
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: ecosystem-cert-preflight-checks
params:
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: ecosystem-cert-preflight-checks
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-ecosystem-cert-preflight-checks:0.2@sha256:1f151e00f7fc427654b7b76045a426bb02fe650d192ffe147a304d2184787e38
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: sast-snyk-check
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: sast-snyk-check
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:351f2dce893159b703e9b6d430a2450b3df9967cb9bd3adb46852df8ccfe4c0d
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
workspace: workspace
- name: clamav-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: clamav-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:cce2dfcc5bd6e91ee54aacdadad523b013eeae5cdaa7f6a4624b8cbcc040f439
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
- name: apply-tags
params:
- name: IMAGE_URL
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: apply-tags
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-apply-tags:0.2@sha256:70881c97a4c51ee1f4d023fa1110e0bdfcfd2f51d9a261fa543c3862b9a4eee9
- name: kind
value: task
resolver: bundles
- name: push-dockerfile
params:
- name: IMAGE
value: $(tasks.build-image-index.results.IMAGE_URL)
- name: IMAGE_DIGEST
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: DOCKERFILE
value: $(params.dockerfile)
- name: CONTEXT
value: $(params.path-context)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: push-dockerfile
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-push-dockerfile:0.1@sha256:d5cb22a833be51dd72a872cac8bfbe149e8ad34da7cb48a643a1e613447a1f9d
- name: kind
value: task
resolver: bundles
workspaces:
- name: workspace
workspace: workspace
- name: rpms-signature-scan
params:
- name: image-digest
value: $(tasks.build-image-index.results.IMAGE_DIGEST)
- name: image-url
value: $(tasks.build-image-index.results.IMAGE_URL)
runAfter:
- build-image-index
taskRef:
params:
- name: name
value: rpms-signature-scan
- name: bundle
value: quay.io/konflux-ci/tekton-catalog/task-rpms-signature-scan:0.2@sha256:1b6c20ab3dbfb0972803d3ebcb2fa72642e59400c77bd66dfd82028bdd09e120
- name: kind
value: task
resolver: bundles
when:
- input: $(params.skip-checks)
operator: in
values:
- "false"
workspaces:
- name: workspace
- name: git-auth
optional: true
- name: netrc
optional: true
taskRunTemplate:
serviceAccountName: build-pipeline-image-builder-frontend
workspaces:
- name: workspace
volumeClaimTemplate:
metadata:
creationTimestamp:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
status: {}
- name: git-auth
secret:
secretName: '{{ git_auth_secret }}'
status: {}

27
.travis.yml Normal file
View file

@ -0,0 +1,27 @@
language: node_js
sudo: required
branches:
only:
- main
- stage-stable
- prod-beta
- prod-stable
notifications:
email: false
node_js:
- '16'
install:
- npm ci
before_script: |
npm run api && [ -z "$(git status --porcelain=v1 2>/dev/null)" ] && echo "✓ No manual API changes." || echo "✗ API manually changed, please refer to the README for the procedure to follow for programmatically generated API endpoints." && [ -z "$(git status --porcelain=v1 2>/dev/null)" ]
script:
- NODE_ENV=production npm run build
- npm run lint
- npm run test
- npx codecov
after_success:
- curl -sSL https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master/src/bootstrap.sh | bash -s
env:
global:
- DEPLOY_REPO="git@github.com:RedHatInsights/image-builder-frontend-build"
- NODE_OPTIONS="--max-old-space-size=4096 --max_old_space_size=4096"

75
.travis/Jenkinsfile vendored Normal file
View file

@ -0,0 +1,75 @@
@Library("github.com/RedHatInsights/insights-pipeline-lib@v3")
import groovy.json.JsonSlurper
node {
stage ("deploy") {
checkout scm
withCredentials(bindings: [sshUserPrivateKey(credentialsId: "cloud-netstorage",
keyFileVariable: "privateKeyFile",
passphraseVariable: "",
usernameVariable: "")]) {
String APP_NAME = "__APP_NAME__"
String BRANCH = env.BRANCH_NAME.replaceAll("origin/", "")
if (BRANCH == "prod-stable") {
PREFIX = ""
} else if (BRANCH == "prod-beta") {
PREFIX = "beta/"
} else if (BRANCH == "qa-stable" || BRANCH == "stage-stable") {
PREFIX = "stage/"
} else if (BRANCH == "qa-beta" || BRANCH == "stage-beta") {
PREFIX = "stage/beta/"
} else {
error "Bug: invalid branch name, we only support (prod/qa/stage)-(beta/stable) and we got ${BRANCH}"
}
// Write build info into app.info.json
// We have the src info there already
def app_info = readJSON file: "./app.info.json"
app_info.build_branch = BRANCH
app_info.build_hash = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
app_info.build_id = env.BUILD_ID
writeJSON file: "./app.info.json", json: app_info
// Send Slack Notification
String SLACK_TEXT = "${APP_NAME}/${BRANCH} [STATUS] - Deploy build ${app_info.build_id} started for GIT COMMIT ${app_info.build_hash}."
slackSend message: SLACK_TEXT, color: 'black', channel: '#insights-bots'
AKAMAI_BASE_PATH = "822386"
AKAMAI_APP_PATH = "/${AKAMAI_BASE_PATH}/${PREFIX}apps/${APP_NAME}"
sh """
eval `ssh-agent`
ssh-add \"$privateKeyFile\"
chmod 600 ~/.ssh/known_hosts ~/.ssh/config
n=0
until [ \$n -ge 10 ]
do
rsync -arv -e \"ssh -2 -o StrictHostKeyChecking=no\" * sshacs@cloud-unprotected.upload.akamai.com:${AKAMAI_APP_PATH} && break
n=\$[\$n+1]
sleep 10
done
"""
//Clear the cache for the app being deployed
openShiftUtils.withJnlpNode(
image: "quay.io/redhatqe/origin-jenkins-agent-akamai:4.9",
namespace: "insights-dev-jenkins"
) {
//install python dependencies
sh "wget https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master/src/akamai_cache_buster/bustCache.py"
sh "wget https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master/src/akamai_cache_buster/requirements.txt"
sh "pip install -r requirements.txt"
withCredentials([file(credentialsId: "jenkins-eccu-cache-purge", variable: 'EDGERC')]) {
//path to .edgerc file is now set to $EDGERC"
//Bust the current cache
sh "python3 bustCache.py $EDGERC ${APP_NAME} ${BRANCH}"
}
// Trigger IQE pipelines
sh ("curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"Trigger IQE pipelines\"}' WEBHOOK_PLACEHOLDER")
}
}
}
}

26
.travis/custom_release.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash
set -ex
if [ "${TRAVIS_BRANCH}" = "main" ]; then
.travis/release.sh "stage-beta"
fi
if [ "${TRAVIS_BRANCH}" = "stage-stable" ]; then
# Download modified Jenkinsfile
curl -o .travis/58231b16fdee45a03a4ee3cf94a9f2c3 https://raw.githubusercontent.com/RedHatInsights/image-builder-frontend/stage-stable/.travis/Jenkinsfile
# Insert stage webhook URL
sed -i 's|WEBHOOK_PLACEHOLDER|https://smee.io/IQDT9yRXsWlqbxpg|g' .travis/58231b16fdee45a03a4ee3cf94a9f2c3
.travis/release.sh "stage-stable"
fi
if [ "${TRAVIS_BRANCH}" = "prod-beta" ]; then
.travis/release.sh "prod-beta"
fi
if [ "${TRAVIS_BRANCH}" = "prod-stable" ]; then
# Download modified Jenkinsfile
curl -o .travis/58231b16fdee45a03a4ee3cf94a9f2c3 https://raw.githubusercontent.com/RedHatInsights/image-builder-frontend/stage-stable/.travis/Jenkinsfile
# Insert prod webhook URL
sed -i 's|WEBHOOK_PLACEHOLDER|https://smee.io/F9gZwIGELxwah4if|g' .travis/58231b16fdee45a03a4ee3cf94a9f2c3
.travis/release.sh "prod-stable"
fi

BIN
.travis/deploy_key.enc Normal file

Binary file not shown.

111
Makefile
View file

@ -1,111 +0,0 @@
PACKAGE_NAME = cockpit-image-builder
INSTALL_DIR_BASE = /share/cockpit/
INSTALL_DIR = $(INSTALL_DIR_BASE)$(PACKAGE_NAME)
APPSTREAMFILE=org.image-builder.$(PACKAGE_NAME).metainfo.xml
VERSION := $(shell (cd "$(SRCDIR)" && grep "^Version:" cockpit/$(PACKAGE_NAME).spec | sed 's/[^[:digit:]]*\([[:digit:]]\+\).*/\1/'))
COMMIT = $(shell (cd "$(SRCDIR)" && git rev-parse HEAD))
# TODO: figure out a strategy for keeping this updated
COCKPIT_REPO_COMMIT = a70142a7a6f9c4e78e71f3c4ec738b6db2fbb04f
COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
# checkout common files from Cockpit repository required to build this project;
# this has no API stability guarantee, so check out a stable tag when you start
# a new project, use the latest release, and update it from time to time
COCKPIT_REPO_FILES = \
pkg/lib \
$(NULL)
help:
@cat Makefile
#
# Install target for specfile
#
.PHONY: install
install:
$(MAKE) cockpit/install
#
# Cockpit related targets
#
.PHONY: cockpit/clean
cockpit/clean:
rm -f cockpit/public/*.css
rm -f cockpit/public/*.js
.PHONY: cockpit/install
cockpit/install:
mkdir -p $(DESTDIR)$(PREFIX)$(INSTALL_DIR)
cp -a cockpit/public/* $(DESTDIR)$(PREFIX)$(INSTALL_DIR)
mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo
msgfmt --xml -d po \
--template cockpit/public/$(APPSTREAMFILE) \
-o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE)
.PHONY: cockpit/devel-uninstall
cockpit/devel-uninstall: PREFIX=~/.local
cockpit/devel-uninstall:
rm -rf $(PREFIX)$(INSTALL_DIR)
.PHONY: cockpit/devel-install
cockpit/devel-install: PREFIX=~/.local
cockpit/devel-install:
PREFIX="~/.local"
mkdir -p $(PREFIX)$(INSTALL_DIR_BASE)
ln -s $(shell pwd)/cockpit/public $(PREFIX)$(INSTALL_DIR)
.PHONY: cockpit/download
cockpit/download: Makefile
@git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \
git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT)
git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x
.PHONY: cockpit/build
cockpit/build: cockpit/download
npm run build:cockpit
.PHONY: cockpit/devel
cockpit/devel: cockpit/devel-uninstall cockpit/build cockpit/devel-install
#
# Building packages
#
RPM_SPEC=cockpit/$(PACKAGE_NAME).spec
NODE_MODULES_TEST=package-lock.json
TARFILE=$(PACKAGE_NAME)-$(VERSION).tar.gz
$(RPM_SPEC): $(RPM_SPEC) $(NODE_MODULES_TEST)
provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \
awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); $(SUB_NODE_ENV) gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@
$(TARFILE): export NODE_ENV ?= production
$(TARFILE): cockpit/build
touch -r package.json package-lock.json
touch cockpit/public/*
tar czf $(TARFILE) --transform 's,^,$(PACKAGE_NAME)/,' \
--exclude node_modules \
$$(git ls-files) $(RPM_SPEC) $(NODE_MODULES_TEST) cockpit/public/ cockpit/README.md
realpath $(TARFILE)
dist: $(TARFILE)
@ls -1 $(TARFILE)
.PHONY: srpm
srpm: $(TARFILE)
rpmbuild -bs \
--define "_sourcedir `pwd`" \
--define "_topdir $(CURDIR)/rpmbuild" \
$(RPM_SPEC)
.PHONY: rpm
rpm: $(TARFILE)
rpmbuild -bb \
--define "_sourcedir `pwd`" \
--define "_topdir $(CURDIR)/rpmbuild" \
$(RPM_SPEC)

291
README.md
View file

@ -1,38 +1,15 @@
# Image Builder Frontend
Frontend code for Image Builder.
## Project
* **Website**: https://www.osbuild.org
* **Bug Tracker**: https://github.com/osbuild/image-builder-frontend/issues
* **Discussions**: https://github.com/orgs/osbuild/discussions
* **Matrix**: #image-builder on [fedoraproject.org](https://matrix.to/#/#image-builder:fedoraproject.org)
## Principles
1. We want to use the latest and greatest web technologies.
2. We want to expose all the options and customizations possible, even if not all are visible by default.
3. The default path should be short(est) clickpath, which should be determined in a data-driven way.
4. This is an [Insights application](https://github.com/RedHatInsights/), so it abides by some rules and standards of Insights.
# image-builder-frontend
## Table of Contents
1. [How to build and run image-builder-frontend](#frontend-development)
1. [Frontend Development](#frontend-development)
2. [Image builder as Cockpit plugin](#image-builder-as-cockpit-plugin)
3. [Backend Development](#backend-development)
2. [API](#api-endpoints)
3. [Unleash feature flags](#unleash-feature-flags)
4. [File structure](#file-structure)
5. [Style Guidelines](#style-guidelines)
6. [Test Guidelines](#test-guidelines)
7. [Running hosted service Playwright tests](#running-hosted-service-playwright-tests)
2. [Backend Development](#backend-development)
2. [File structure](#file-structure)
3. [Style Guidelines](#style-guidelines)
4. [Test Guidelines](#test-guidelines)
## How to build and run image-builder-frontend
> [!IMPORTANT]
> Running image-builder-frontend against [console.redhat.com](https://console.redhat.com/) requires connection to the Red Hat VPN, which is only available to Red Hat employees. External contributors can locally run [image builder as Cockpit plugin](#image-builder-as-cockpit-plugin).
### Frontend Development
To develop the frontend you can use a proxy to run image-builder-frontend locally
@ -43,15 +20,16 @@ worrying if a feature from stage has been released yet.
#### Nodejs and npm version
Make sure you have npm@10 and node 22+ installed. If you need multiple versions of nodejs check out [nvm](https://github.com/nvm-sh/nvm).
Make sure you have npm@7 and node 15+ installed. If you need multiple versions of nodejs check out [nvm](https://github.com/nvm-sh/nvm).
#### Webpack proxy
1. run `npm ci`
2. run `npm run start:prod`
2. run `npm run prod-beta`. This command uses a prod-beta env by default. Configure your
environment by the `env` attribute in `dev.webpack.config.js`.
3. redirect `prod.foo.redhat.com` to localhost, if this has not been done already
3. Secondly redirect a few `prod.foo.redhat.com` to localhost, if this has not been done already.
```bash
echo "127.0.0.1 prod.foo.redhat.com" >> /etc/hosts
@ -63,9 +41,10 @@ echo "127.0.0.1 prod.foo.redhat.com" >> /etc/hosts
1. run `npm ci`
2. run `npm run start:stage`
2. run `npm run stage-beta`. This command uses a stage-beta env by default. Configure your
environment by the `env` attribute in `dev.webpack.config.js`.
3. redirect `stage.foo.redhat.com` to localhost, if this has not been done already
3. Secondly redirect a few `stage.foo.redhat.com` to localhost, if this has not been done already.
```bash
echo "127.0.0.1 stage.foo.redhat.com" >> /etc/hosts
@ -73,73 +52,51 @@ echo "127.0.0.1 stage.foo.redhat.com" >> /etc/hosts
4. open browser at `https://stage.foo.redhat.com:1337/beta/insights/image-builder`
### Image builder as Cockpit plugin
#### Insights proxy (deprecated)
> [!NOTE]
> Issues marked with [cockpit-image-builder](https://github.com/osbuild/image-builder-frontend/issues?q=is%3Aissue%20state%3Aopen%20label%3Acockpit-image-builder) label are reproducible in image builder plugin and can be worked on by external contributors without connection to the Red Hat VPN.
1. Clone the insights proxy: https://github.com/RedHatInsights/insights-proxy
#### Cockpit setup
To install and setup Cockpit follow guide at: https://cockpit-project.org/running.html
2. Setting up the proxy
#### On-premises image builder installation and configuration
To install and configure `osbuild-composer` on your local machine follow our documentation: https://osbuild.org/docs/on-premises/installation/
Choose a runner (podman or docker), and point the SPANDX_CONFIG variable to
`profile/local-frontend.js` included in image-builder-frontend.
#### Scripts for local development of image builder plugin
```
sudo insights-proxy/scripts/patch-etc-hosts.sh
export RUNNER="podman"
export SPANDX_CONFIG=$PATH_TO/image-builder-frontend/profiles/local-frontend.js
sudo -E insights-proxy/scripts/run.sh
```
The following scripts are used to build the frontend with Webpack and install it into the Cockpit directories. These scripts streamline the development process by automating build and installation steps.
3. Starting up image-builder-frontend
Runs Webpack with the specified configuration (cockpit/webpack.config.ts) to build the frontend assets.
Use this command whenever you need to compile the latest changes in your frontend code.
In the image-builder-frontend checkout directory
Creates the necessary directory in the user's local Cockpit share (~/.local/share/cockpit/).
Creates a symbolic link (image-builder-frontend) pointing to the built frontend assets (cockpit/public).
Use this command after building the frontend to install it locally for development purposes.
The symbolic link allows Cockpit to serve the frontend assets from your local development environment,
making it easier to test changes in real-time without deploying to a remote server.
```
npm install
npm start
```
```bash
make cockpit/build
```
The UI should be running on
https://prod.foo.redhat.com:1337/beta/insights/image-builder/landing.
Note that this requires you to have access to either production or stage (plus VPN and proxy config) of insights.
```bash
make cockpit/devel-install
```
#### API endpoints
To uninstall and remove the symbolic link, run the following command:
API endpoints are programmatically generated with the RTKQ library. This
sections overview the steps to add new APIs and endpoints.
```bash
make cockpit/devel-uninstall
```
##### Add a new API
For convenience, you can run the following to combine all three steps:
For an hypothetical API called foobar
1. Download the foobar api openapi json or yaml representation under
`api/schema/foobar.json`
```bash
make cockpit/devel
```
### Backend Development
To develop both the frontend and the backend you can again use the proxy to run both the
frontend and backend locally against the chrome at cloud.redhat.com. For instructions
see the [osbuild-getting-started project](https://github.com/osbuild/osbuild-getting-started).
## API endpoints
API slice definitions are programmatically generated using the [@rtk-query/codegen-openapi](https://redux-toolkit.js.org/rtk-query/usage/code-generation) package.
The OpenAPI schema are imported during code generation. OpenAPI configuration files are
stored in `/api/config`. Each endpoint has a corresponding empty API slice and generated API
slice which are stored in `/src/store`.
### Add a new API schema
For a hypothetical API called foobar
1. Create a new "empty" API file under `src/store/emptyFoobarApi.ts` that has following
2. Create a new "empty" api file under `src/store/emptyFoobarApi.ts` that has for
content:
```typescript
```{ts}
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { FOOBAR_API } from '../constants';
@ -147,26 +104,26 @@ import { FOOBAR_API } from '../constants';
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptyFoobarApi = createApi({
reducerPath: 'foobarApi',
baseQuery: fetchBaseQuery({ baseUrl: window.location.origin + FOO_BAR }),
baseQuery: fetchBaseQuery({ baseUrl: FOO_BAR }),
endpoints: () => ({}),
});
```
2. Declare new constant `FOOBAR_API` with the API url in `src/constants.ts`
3. Declare the new constat `FOOBAR_API` to the API url in `src/constants.js`
```typescript
```
export const FOOBAR_API = 'api/foobar/v1'
```
3. Create the config file for code generation in `api/config/foobar.ts` containing:
4. Create the config file for code generation in `api/config/foobar.ts` containing:
```typescript
```
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: 'URL_TO_THE_OPENAPI_SCHEMA',
schemaFile: '../schema/foobar.json',
apiFile: '../../src/store/emptyFoobarApi.ts',
apiImport: 'emptyContentSourcesApi',
apiImport: 'emptyEdgeApi',
outputFile: '../../src/store/foobarApi.ts',
exportName: 'foobarApi',
hooks: true,
@ -174,29 +131,34 @@ const config: ConfigFile = {
};
```
4. Update the `eslint.config.js` file by adding the generated code path to the ignores array:
5. Update the `api.sh` script by adding a new line for npx to generate the code:
```
ignores: [
<other ignored files>,
'**/foobarApi.ts',
]
npx @rtk-query/codegen-openapi ./api/config/foobar.ts &
```
5. run api generation
```bash
6. Update the `.eslintignore` file by adding a new line for the generated code:
```
foobarApi.ts
```
7. run api generation
```
npm run api
```
And voilà!
### Add a new endpoint
##### Add a new endpoint
To add a new endpoint, simply update the `api/config/foobar.ts` file with new
endpoints in the `filterEndpoints` table.
## Unleash feature flags
#### Unleash feature flags for the frontend
Your user needs to have the corresponding rights, do the
same as this MR in internal gitlab https://gitlab.cee.redhat.com/service/app-interface/-/merge_requests/79225
@ -212,49 +174,47 @@ existing flags:
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/Components/ImagesTable/ImageLink.js#L99
### Mocking flags for tests
##### Mocking flags for tests
Flags can be mocked for the unit tests to access some feature. Checkout:
https://github.com/osbuild/image-builder-frontend/blob/9a464e416bc3769cfc8e23b62f1dd410eb0e0455/src/test/Components/CreateImageWizard/CreateImageWizard.test.tsx#L49
https://github.com/RedHatInsights/image-builder-frontend/blob/c84b493eba82ce83a7844943943d91112ffe8322/src/test/Components/CreateImageWizard/CreateImageWizard.test.js#L74
If the two possible code path accessible via the toggles are defined in the code
base, then it's good practice to test the two of them. If not, only test what's
actually owned by the frontend project.
### Cleaning the flags
##### Cleaning the flags
Unleash toggles are expected to live for a limited amount of time, documentation
specify 40 days for a release, we should keep that in mind for each toggle
we're planning on using.
### Backend Development
To develop both the frontend and the backend you can again use the proxy to run both the
frontend and backend locally against the chrome at cloud.redhat.com. For instructions
see [devel/README.md](devel/README.md).
## File Structure
### Quick Reference
| Directory | Description |
| --------- | ----------- |
| [`/api`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/api) | API schema and config files |
| [`/config`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/config) | webpack configuration |
| [`/devel`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/devel) | tools for local development |
| [`/src`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src) | source code |
| [`/src/Components`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/Components) | source code split by individual components |
| [`/src/test`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/test) | test utilities |
| [`/src/test/mocks`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/test/mocks) | mock handlers and server config for MSW |
| [`/src/store`](https://github.com/RedHatInsights/image-builder-frontend/tree/main/src/store) | Redux store |
| [`/src/api.js`](https://github.com/RedHatInsights/image-builder-frontend/blob/main/src/api.js) | API calls |
## Style Guidelines
This project uses recommended rule sets rom several plugins:
- `@eslint/js`
- `typescript-eslint`
- `eslint-plugin-react`
- `eslint-plugin-react-hooks`
- `eslint-plugin-react-redux`
- `eslint-plugin-import`
- `eslint-plugin-jsx-a11y`
- `eslint-plugin-disable-autofix`
- `eslint-plugin-jest-dom`
- `eslint-plugin-testing-library`
- `eslint-plugin-playwright`
- `@redhat-cloud-services/eslint-config-redhat-cloud-services`
This project uses eslint's recommended styling guidelines. These rules can be found here:
https://eslint.org/docs/rules/
To run the linter, use:
```bash
@ -263,14 +223,20 @@ npm run lint
Any errors that can be fixed automatically, can be corrected by running:
```bash
npm run lint:js:fix
npm run lint --fix
```
All the linting rules and configuration of ESLint can be found in [`eslint.config.js`](https://github.com/RedHatInsights/image-builder-frontend/blob/main/eslint.config.js).
All the linting rules and configuration of eslint can be found in [`.eslintrc.yml`](https://github.com/RedHatInsights/image-builder-frontend/blob/main/.eslintrc.yml).
### Additional eslint rules
There are also additional rules added to enforce code style. Those being:
- `import/order` -> enforces the order in import statements and separates them into groups based on their type
- `prefer-const` -> enforces use of `const` declaration for variables that are never reassigned
- `no-console` -> throws an error for any calls of `console` methods leftover after debugging
## Test Guidelines
This project is tested using the [Vitest](https://vitest.dev/guide/) framework, [React Testing Library](https://testing-library.com/docs/react-testing-library/intro), and the [Mock Service Worker](https://mswjs.io/docs/) library.
This project is tested using the [Jest](https://jestjs.io/docs/getting-started) framework, [React Testing Library](https://testing-library.com/docs/react-testing-library/intro), and the [Mock Service Worker](https://mswjs.io/docs/) library.
All UI contributions must also include a new test or update an existing test in order to maintain code coverage.
@ -281,79 +247,18 @@ To run the unit tests, the linter, and the code coverage check run:
npm run test
```
These tests will also be run in our CI when a PR is opened.
These tests will also be run in our Travis CI when a PR is opened.
Note that `testing-library` DOM printout is currently disabled for all tests by the following configuration in `src/test/setup.ts`:
```typescript
configure({
getElementError: (message: string) => {
const error = new Error(message);
error.name = 'TestingLibraryElementError';
error.stack = '';
return error;
},
});
## API endpoints
API slice definitions are generated using the [@rtk-query/codegen-openapi](https://redux-toolkit.js.org/rtk-query/usage/code-generation) package.
OpenAPI schema for the endpoints are stored in `/api/schema`. Their
corresponding configuration files are stored in `/api/config`. Each endpoint
has a corresponding empty API slice and generated API slice which are stored in
`/src/store`.
To generate or update API slice definitions, run:
```bash
npm run api
```
If you'd like to see the stack printed out you can either temporarily disable the configuration or generate a [Testing Playground](https://testing-playground.com/) link by adding `screen.logTestingPlaygroundURL()` to your test.
### ~~Using MSW data in development~~ - CURRENTLY NOT WORKING
If you want to develop in environment with mocked data, run the command `npm run stage-beta:msw`.
#### Enabling MSW
In a case you're seeing `Error: [MSW] Failed to register the Service Worker` in console, you might also need to configure SSL certification on your computer.
In order to do this install [mkcert](https://github.com/FiloSottile/mkcert)
After the installation, go to the `/node_modules/.cache/webpack-dev-server` folder and run following commands:
1. `mkcert -install`  to create a new certificate authority on your machine
2. `mkcert prod.foo.redhat.com`  to create the actual signed certificate
#### Mac Configuration
Follow these steps to find and paste the certification file into the 'Keychain Access' application:
1. Open the 'Keychain Access' application.
2. Select 'login' on the left side.
3. Navigate to the 'Certificates' tab.
4. Drag the certification file (located at /image-builder-frontend/node_modules/.cache/webpack-dev-server/server.pem) to the certification list.
5. Double-click on the added certificate (localhost certificate) to open the localhost window.
6. Open the 'Trust' dropdown menu.
7. Set all options to 'Always Trust'.
8. Close the localhost screen.
9. Run `npm run stage-beta:msw` and open the Firefox browser to verify that it is working as expected.
## Running hosted service Playwright tests
1. Copy the [example env file](playwright_example.env) content and create a file named `.env` in the root directory of the project. Paste the example file content into it.
For local development fill in the:
* `BASE_URL` - `https://stage.foo.redhat.com:1337` is required, which is already set in the example config
* `PLAYWRIGHT_USER` - your consoledot stage username
* `PLAYWRIGHT_PASSWORD` - your consoledot stage password
2. Make sure Playwright is installed as a dev dependency
```bash
npm ci
```
3. Download the Playwright browsers with
```bash
npx playwright install
```
4. Start the local development stage server by running
```bash
npm run start:stage
```
5. Now you have two options of how to run the tests:
* (Preferred) Use VS Code and the [Playwright Test module for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright). But other editors do have similar plugins for ease of use, if so desired
* Using terminal - `npx playwright test` will run the playwright test suite. `npx playwright test --headed` will run the suite in a vnc-like browser so you can watch it's interactions.

View file

@ -5,8 +5,7 @@ npx @rtk-query/codegen-openapi ./api/config/imageBuilder.ts &
npx @rtk-query/codegen-openapi ./api/config/rhsm.ts &
npx @rtk-query/codegen-openapi ./api/config/contentSources.ts &
npx @rtk-query/codegen-openapi ./api/config/provisioning.ts &
npx @rtk-query/codegen-openapi ./api/config/compliance.ts &
npx @rtk-query/codegen-openapi ./api/config/composerCloudApi.ts &
npx @rtk-query/codegen-openapi ./api/config/edge.ts &
# Wait for all background jobs to finish
wait

View file

@ -1,19 +0,0 @@
# API
This folder contains generated code for API endpoints needed by this repo.
## Updating API changes
To pull new API changes and regenerating the code run
```shell
npm run api
```
## Regenerating the code only
To regenerate the code only, run
```shell
npm run api:generate
```

View file

@ -1,14 +0,0 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: 'https://console.redhat.com/api/compliance/v2/openapi.json',
apiFile: '../../src/store/service/emptyComplianceApi.ts',
apiImport: 'emptyComplianceApi',
outputFile: '../../src/store/service/complianceApi.ts',
exportName: 'complianceApi',
hooks: true,
unionUndefined: true,
filterEndpoints: ['policies', 'policy'],
};
export default config;

View file

@ -1,15 +0,0 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile:
'https://raw.githubusercontent.com/osbuild/osbuild-composer/main/internal/cloudapi/v2/openapi.v2.yml',
apiFile: '../../src/store/cockpit/emptyComposerCloudApi.ts',
apiImport: 'emptyComposerCloudApi',
outputFile: '../../src/store/cockpit/composerCloudApi.ts',
exportName: 'composerCloudApi',
hooks: false,
unionUndefined: true,
filterEndpoints: ['postCompose', 'getComposeStatus'],
};
export default config;

View file

@ -1,26 +1,13 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: 'https://console.redhat.com/api/content-sources/v1/openapi.json',
apiFile: '../../src/store/service/emptyContentSourcesApi.ts',
schemaFile: '../schema/contentSources.json',
apiFile: '../../src/store/emptyContentSourcesApi.ts',
apiImport: 'emptyContentSourcesApi',
outputFile: '../../src/store/service/contentSourcesApi.ts',
outputFile: '../../src/store/contentSourcesApi.ts',
exportName: 'contentSourcesApi',
hooks: true,
unionUndefined: true,
filterEndpoints: [
'createRepository',
'listRepositories',
'listRepositoriesRpms',
'listRepositoryParameters',
'searchRpm',
'searchPackageGroup',
'listFeatures',
'listSnapshotsByDate',
'bulkImportRepositories',
'listTemplates',
'getTemplate',
],
filterEndpoints: ['listRepositories', 'listRepositoriesRpms'],
};
export default config;

35
api/config/edge.ts Normal file
View file

@ -0,0 +1,35 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: '../schema/edge.json',
apiFile: '../../src/store/emptyEdgeApi.ts',
apiImport: 'emptyEdgeApi',
outputFile: '../../src/store/edgeApi.ts',
exportName: 'edgeApi',
hooks: true,
filterEndpoints: [
'createImage',
'createImageUpdate',
'getAllImages',
'getImageStatusByID',
'getImageByID',
'getImageDetailsByID',
'getImageByOstree',
'createInstallerForImage',
'getRepoForImage',
'getMetadataForImage',
'createKickStartForImage',
'checkImageName',
'retryCreateImage',
'listAllImageSets',
'getImageSetsByID',
'getImageSetsView',
'getImageSetViewByID',
'getAllImageSetImagesView',
'getImageSetsDevicesByID',
'deleteImageSet',
'getImageSetImageView',
],
};
export default config;

View file

@ -1,14 +1,12 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile:
'https://raw.githubusercontent.com/osbuild/image-builder/main/internal/v1/api.yaml',
apiFile: '../../src/store/service/emptyImageBuilderApi.ts',
schemaFile: '../schema/imageBuilder.yaml',
apiFile: '../../src/store/emptyImageBuilderApi.ts',
apiImport: 'emptyImageBuilderApi',
outputFile: '../../src/store/service/imageBuilderApi.ts',
outputFile: '../../src/store/imageBuilderApi.ts',
exportName: 'imageBuilderApi',
hooks: { queries: true, lazyQueries: true, mutations: true },
unionUndefined: true,
hooks: true,
filterEndpoints: [
'cloneCompose',
'composeImage',
@ -20,17 +18,6 @@ const config: ConfigFile = {
'getPackages',
'getOscapProfiles',
'getOscapCustomizations',
'getOscapCustomizationsForPolicy',
'createBlueprint',
'updateBlueprint',
'composeBlueprint',
'getBlueprints',
'exportBlueprint',
'getBlueprintComposes',
'deleteBlueprint',
'getBlueprint',
'recommendPackage',
'fixupBlueprint',
],
};

View file

@ -1,13 +1,12 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: 'https://console.redhat.com/api/provisioning/v1/openapi.json',
apiFile: '../../src/store/service/emptyProvisioningApi.ts',
schemaFile: '../schema/provisioning.json',
apiFile: '../../src/store/emptyProvisioningApi.ts',
apiImport: 'emptyProvisioningApi',
outputFile: '../../src/store/service/provisioningApi.ts',
outputFile: '../../src/store/provisioningApi.ts',
exportName: 'provisioningApi',
hooks: true,
unionUndefined: true,
filterEndpoints: ['getSourceList', 'getSourceUploadInfo'],
};

View file

@ -1,18 +1,13 @@
import type { ConfigFile } from '@rtk-query/codegen-openapi';
const config: ConfigFile = {
schemaFile: 'https://console.redhat.com/api/rhsm/v2/openapi.json',
apiFile: '../../src/store/service/emptyRhsmApi.ts',
schemaFile: '../schema/rhsm.json',
apiFile: '../../src/store/emptyRhsmApi.ts',
apiImport: 'emptyRhsmApi',
outputFile: '../../src/store/service/rhsmApi.ts',
outputFile: '../../src/store/rhsmApi.ts',
exportName: 'rhsmApi',
hooks: true,
unionUndefined: true,
filterEndpoints: [
'listActivationKeys',
'showActivationKey',
'createActivationKeys',
],
filterEndpoints: ['listActivationKeys', 'showActivationKey'],
};
export default config;

File diff suppressed because it is too large Load diff

5722
api/schema/edge.json Normal file

File diff suppressed because it is too large Load diff

1181
api/schema/imageBuilder.yaml Normal file

File diff suppressed because it is too large Load diff

2044
api/schema/provisioning.json Normal file

File diff suppressed because it is too large Load diff

1
api/schema/rhsm.json Normal file

File diff suppressed because one or more lines are too long

32
babel.config.js Normal file
View file

@ -0,0 +1,32 @@
// copied from https://github.com/RedHatInsights/frontend-starter-app/blob/master/babel.config.js
module.exports = {
presets: [
// Polyfills
'@babel/env',
'@babel/react',
'@babel/typescript',
],
plugins: [
// Put _extends helpers in their own file
'@babel/plugin-transform-runtime',
// Support for {...props} via Object.assign({}, props)
'@babel/plugin-proposal-object-rest-spread',
// Devs tend to write `import { someIcon } from '@patternfly/react-icons';`
// This transforms the import to be specific which prevents having to parse 2k+ icons
// Also prevents potential bundle size blowups with CJS
[
'transform-imports',
{
'@patternfly/react-icons': {
transform: (importName) =>
`@patternfly/react-icons/dist/js/icons/${importName
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase()}`,
preventFullImport: true,
},
},
'react-icons',
],
],
};

@ -1 +0,0 @@
Subproject commit b496d0a8c1755608bd256a6960869b14a7689d38

View file

@ -9,7 +9,7 @@ export COMPONENT="image-builder"
export IMAGE="quay.io/cloudservices/image-builder-frontend"
export APP_ROOT=$(pwd)
export WORKSPACE=${WORKSPACE:-$APP_ROOT} # if running in jenkins, use the build's workspace
export NODE_BUILD_VERSION=22
export NODE_BUILD_VERSION=16
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
set -exv

View file

@ -1,32 +0,0 @@
# codebuild buildspec
version: 0.2
run-as: root
phases:
install:
commands:
- echo Entered the install phase...
- nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
pre_build:
commands:
- echo Entered the pre_build phase...
build:
commands:
- echo Entered the build phase...
post_build:
commands:
- echo Entered the post_build phase...
cache:
paths:
- "/root/.cache/ms-playwright"
- "/root/.docker"
- "/root/.npm"
- "/root/.yarn"
- "/root/.cache/go-build"
- "/root/go"
- "/root/.composer/cache"

33
ci.sh
View file

@ -1,33 +0,0 @@
#!/bin/bash
# Workaround needed for Konflux pipeline to pass
find -name "cockpit" -type d -maxdepth 1
find -name "cockpit" -type d -maxdepth 1 | xargs rm -rf -
setNpmOrYarn
install
build
if [ "$IS_PR" == true ]; then
verify
else
export BETA=false
build
source build_app_info.sh
mv ${DIST_FOLDER} stable
export BETA=true
# Export sentry specific variables for the webpack plugin. Note that
# this only works in jenkins (not konflux). The webpack plugin will
# both inject debug ids and upload the sourcemaps, in konflux only
# the debug ids are injected. As the debug ids are consistend
# across builds, this works.
export SENTRY_AUTH_TOKEN
build
source build_app_info.sh
mv ${DIST_FOLDER} preview
mkdir -p ${DIST_FOLDER}
mv stable ${DIST_FOLDER}/stable
mv preview ${DIST_FOLDER}/preview
fi
# End workaround

View file

@ -1,3 +0,0 @@
# cockpit-image-builder
The "cockpit-image-builder" provides an on-premise frontend for image building, designed to integrate with [Cockpit](https://cockpit-project.org/) as a plugin. It allows users to create, manage, and compose custom operating system images, with images stored locally.

View file

@ -1,53 +0,0 @@
Name: cockpit-image-builder
Version: 76
Release: 1%{?dist}
Summary: Image builder plugin for Cockpit
License: Apache-2.0
URL: http://osbuild.org/
Source0: https://github.com/osbuild/image-builder-frontend/releases/download/v%{version}/%{name}-%{version}.tar.gz
Obsoletes: cockpit-composer < 54
Provides: cockpit-composer = %{version}-%{release}
BuildArch: noarch
BuildRequires: gettext
BuildRequires: libappstream-glib
BuildRequires: make
BuildRequires: nodejs
Requires: cockpit
Requires: cockpit-files
Requires: osbuild-composer >= 131
%description
The image-builder-frontend generates custom images suitable for
deploying systems or uploading to the cloud. It integrates into Cockpit
as a frontend for osbuild.
%prep
%setup -q -n %{name}
%build
# Nothing to build
%install
%make_install PREFIX=/usr
# drop source maps, they are large and just for debugging
find %{buildroot}%{_datadir}/cockpit/ -name '*.map' | xargs --no-run-if-empty rm --verbose
%check
appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*
%files
%doc cockpit/README.md
%license LICENSE
%{_datadir}/cockpit/cockpit-image-builder
%{_datadir}/metainfo/*
%changelog
# the changelog is distribution-specific, therefore there's just one entry
# to make rpmlint happy.
* Mon Jan 13 2025 Image Builder team <osbuilders@redhat.com> - 0-1
- The changelog was added to the rpm spec file.

View file

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en-us" class="layout-pf pf-m-redhat-font">
<head>
<meta charset="utf-8" />
<title>Image-Builder</title>
<!-- js dependencies -->
<script type="text/javascript" src="../base1/cockpit.js"></script>
<script defer src="main.js"></script>
<link href="main.css" rel="stylesheet" />
</head>
<body>
<div class="ct-page-fill" id="main"></div>
</body>
</html>

View file

@ -1,15 +0,0 @@
{
"dashboard": {
"index": {
"label": "Image Builder",
"icon": "pficon-build",
"docs": [
{
"label": "Creating system images",
"url": "https://osbuild.org/"
}
]
}
},
"content-security-policy": "default-src 'self' 'unsafe-eval'"
}

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="addon">
<id>org.image-builder.cockpit-image-builder</id>
<metadata_license>CC0-1.0</metadata_license>
<name>Image Builder</name>
<summary>
Build customized operating system images
</summary>
<description>
<p>
Image Builder can generate custom images suitable for deploying
systems, or as images ready to upload to the cloud.
</p>
</description>
<extends>org.cockpit_project.cockpit</extends>
<url type="homepage">https://github.com/osbuild/image-builder-frontend/</url>
<launchable type="cockpit-manifest">cockpit-image-builder</launchable>
</component>

View file

@ -1,14 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
// this allows us to pull in the `cockpit` and
// `cockpit/fsinfo` modules from the `pkg/lib`
// directory
"../pkg/lib/*"
]
}
}
}

View file

@ -1,90 +0,0 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack'); // Add this line
const [mode, devtool] =
process.env.NODE_ENV === 'production'
? ['production', 'source-map']
: ['development', 'inline-source-map'];
const output = {
path: path.resolve('cockpit/public'),
filename: 'main.js',
sourceMapFilename: '[file].map',
};
const plugins = [
new MiniCssExtractPlugin({
ignoreOrder: true,
}),
new webpack.DefinePlugin({
'process.env.IS_ON_PREMISE': JSON.stringify(true),
}),
];
module.exports = {
entry: './src/AppCockpit.tsx',
output,
mode,
devtool,
plugins,
devServer: {
historyApiFallback: true, // Ensures all routes are served with `index.html`
},
resolve: {
fallback: {
path: require.resolve('path-browserify'),
},
modules: [
'node_modules',
// this tells webpack to check `node_modules`
// and `pkg/lib` for modules. This allows us
// to import `cockpit` and `cockpit/fsinfo`
path.resolve(__dirname, '../pkg/lib'),
],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
include: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../pkg/lib'),
],
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
},
},
resolve: { fullySpecified: false },
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { url: false },
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: { url: false },
},
'sass-loader',
],
},
],
},
};

View file

@ -1,16 +1,12 @@
coverage:
status:
patch: off
patch: no
project:
default:
threshold: 5%
codecov:
require_ci_to_pass: false
require_ci_to_pass: no
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false
ignore:
- "api"
- "playwright"
- "src/store"
require_changes: no

View file

@ -0,0 +1,112 @@
const { resolve } = require('path');
const config = require('@redhat-cloud-services/frontend-components-config');
const CopyPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const webpackProxy = {
useProxy: true,
proxyVerbose: true,
env: `${process.env.STAGE ? 'stage' : 'prod'}-${
process.env.BETA ? 'beta' : 'stable'
}`,
appUrl: [
'/insights/image-builder',
'/beta/insights/image-builder',
'/preview/insights/image-builder',
],
routes: {
...(process.env.CONFIG_PORT && {
[`${process.env.BETA ? '/beta' : ''}/config`]: {
host: `http://localhost:${process.env.CONFIG_PORT}`,
},
}),
...(process.env.LOCAL_API && {
...(process.env.LOCAL_API.split(',') || []).reduce((acc, curr) => {
const [appName, appConfig] = (curr || '').split(':');
const [appPort = 8003, protocol = 'http', host = 'localhost'] =
appConfig.split('~');
return {
...acc,
[`/apps/${appName}`]: { host: `${protocol}://${host}:${appPort}` },
[`/beta/apps/${appName}`]: {
host: `${protocol}://${host}:${appPort}`,
},
};
}, {}),
}),
},
};
const { config: webpackConfig, plugins } = config({
rootFolder: resolve(__dirname, '../'),
debug: true,
useFileHash: false,
sassPrefix: '.imageBuilder',
deployment: process.env.BETA ? 'beta/apps' : 'apps',
...(process.env.PROXY ? webpackProxy : {}),
});
plugins.push(
require('@redhat-cloud-services/frontend-components-config/federated-modules')(
{
root: resolve(__dirname, '../'),
useFileHash: false,
exposes: {
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
},
shared: [{ 'react-router-dom': { singleton: true } }],
exclude: ['react-router-dom'],
}
)
);
if (process.env.MSW) {
// Copy mockServiceWorker.js to ./dist/ so it is served with the bundle
plugins.push(
new CopyPlugin({
patterns: [
{ from: 'src/mockServiceWorker.js', to: 'mockServiceWorker.js' },
],
})
);
/*
mockServiceWorker.js will be served from /beta/apps/image-builder, which
will become its default scope. Setting the Service-Worker-Allowed header to
'/' allows the worker's scope to be expanded to the root route '/'.
The default webpackConfig for stage does not contain any headers.
Caution: The default webpackConfig for prod *does* contain headers, so this
code will need to be modified if using MSW in prod-beta or prod-stable so that
those headers are not overwritten.
*/
webpackConfig.devServer.headers = { 'Service-Worker-Allowed': '/' };
/*
We would like the client to be able to determine whether or not to start
the service worker at run time based on the value of process.env.MSW. We can
add that variable to process.env via the DefinesPlugin plugin, but
DefinePlugin has already been added by config() to the default webpackConfig.
Therefore, we find it in the `plugins` array based on its type, then update
it to add our new process.env.MSW variable.
*/
const definePluginIndex = plugins.findIndex(
(plugin) => plugin instanceof webpack.DefinePlugin
);
const definePlugin = plugins[definePluginIndex];
const newDefinePlugin = new webpack.DefinePlugin({
...definePlugin.definitions,
'process.env.MSW': true,
});
plugins[definePluginIndex] = newDefinePlugin;
}
module.exports = {
...webpackConfig,
plugins,
};

View file

@ -0,0 +1,39 @@
const { resolve } = require('path');
const config = require('@redhat-cloud-services/frontend-components-config');
const { config: webpackConfig, plugins } = config({
rootFolder: resolve(__dirname, '../'),
debug: true,
useFileHash: false,
sassPrefix: '.imageBuilder',
deployment: 'beta/apps',
appUrl: '/preview/insights/image-builder',
env: 'stage-beta',
useProxy: true,
useAgent: true,
bounceProd: false,
proxyVerbose: true,
routes: {
'/api/image-builder/v1': { host: 'http://localhost:8086' },
},
});
plugins.push(
require('@redhat-cloud-services/frontend-components-config/federated-modules')(
{
root: resolve(__dirname, '../'),
useFileHash: false,
exposes: {
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
},
shared: [{ 'react-router-dom': { singleton: true } }],
exclude: ['react-router-dom'],
}
)
);
module.exports = {
...webpackConfig,
plugins,
};

View file

@ -0,0 +1,25 @@
const { resolve } = require('path');
const config = require('@redhat-cloud-services/frontend-components-config');
const { config: webpackConfig, plugins } = config({
rootFolder: resolve(__dirname, '../'),
sassPrefix: '.imageBuilder',
});
plugins.push(
require('@redhat-cloud-services/frontend-components-config/federated-modules')(
{
root: resolve(__dirname, '../'),
exposes: {
'./RootApp': resolve(__dirname, '../src/AppEntry.js'),
},
shared: [{ 'react-router-dom': { singleton: true } }],
exclude: ['react-router-dom'],
}
)
);
module.exports = {
...webpackConfig,
plugins,
};

View file

@ -1,3 +0,0 @@
{
"/apps/image-builder*": { "url": "http://127.0.0.1:8003" }
}

12
distribution/Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM node:18
WORKDIR /app
COPY . .
RUN npm ci
EXPOSE 8002
EXPOSE 1337
CMD [ "npm", "run", "devel" ]

View file

@ -1,174 +0,0 @@
const js = require('@eslint/js');
const tseslint = require('typescript-eslint');
const pluginReact = require('eslint-plugin-react');
const pluginReactHooks = require('eslint-plugin-react-hooks');
const pluginReactRedux = require('eslint-plugin-react-redux');
const pluginImport = require('eslint-plugin-import');
const fecConfig = require('@redhat-cloud-services/eslint-config-redhat-cloud-services');
const pluginJsxA11y = require('eslint-plugin-jsx-a11y');
const disableAutofix = require('eslint-plugin-disable-autofix');
const pluginPrettier = require('eslint-plugin-prettier');
const jestDom = require('eslint-plugin-jest-dom');
const pluginTestingLibrary = require('eslint-plugin-testing-library');
const pluginPlaywright = require('eslint-plugin-playwright');
const { defineConfig } = require('eslint/config');
const globals = require('globals');
module.exports = defineConfig([
{ // Ignore programatically generated files
ignores: [
'**/mockServiceWorker.js',
'**/imageBuilderApi.ts',
'**/contentSourcesApi.ts',
'**/rhsmApi.ts',
'**/provisioningApi.ts',
'**/complianceApi.ts',
'**/composerCloudApi.ts'
]
},
{ // Base config for js/ts files
files: ['**/*.{js,ts,jsx,tsx}'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: './tsconfig.json'
},
globals: {
...globals.browser,
// node
'JSX': 'readonly',
'process': 'readonly',
'__dirname': 'readonly',
'require': 'readonly',
// vitest
'describe': 'readonly',
'it': 'readonly',
'test': 'readonly',
'expect': 'readonly',
'vi': 'readonly',
'beforeAll': 'readonly',
'beforeEach': 'readonly',
'afterAll': 'readonly',
'afterEach': 'readonly'
},
},
plugins: {
js,
'@typescript-eslint': tseslint.plugin,
react: pluginReact,
'react-hooks': pluginReactHooks,
'react-redux': pluginReactRedux,
import: pluginImport,
jsxA11y: pluginJsxA11y,
'disable-autofix': disableAutofix,
prettier: pluginPrettier,
},
rules: {
...js.configs.recommended.rules,
...tseslint.configs.recommended.rules,
...pluginReact.configs.flat.recommended.rules,
...pluginReactHooks.configs.recommended.rules,
...pluginReactRedux.configs.recommended.rules,
...fecConfig.rules,
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'sibling', 'parent', 'index'],
alphabetize: {
order: 'asc',
caseInsensitive: true
},
'newlines-between': 'always',
pathGroups: [ // ensures the import of React is always on top
{
pattern: 'react',
group: 'builtin',
position: 'before'
}
],
pathGroupsExcludedImportTypes: ['react']
}],
'sort-imports': ['error', {
ignoreCase: true,
ignoreDeclarationSort: true,
ignoreMemberSort: false,
}],
'no-duplicate-imports': 'error',
'prefer-const': ['error', {
destructuring: 'any',
}],
'no-console': 'error',
'eqeqeq': 'error',
'array-callback-return': 'warn',
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': 'allow-with-description',
'ts-nocheck': true,
'ts-check': true,
minimumDescriptionLength: 5,
}],
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/no-require-imports': 'error',
'disable-autofix/@typescript-eslint/no-unnecessary-condition': 'warn',
'no-unused-vars': 'off', // disable js rule in favor of @typescript-eslint's rule
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'jsx-a11y/no-autofocus': 'off',
'prettier/prettier': ['error', {
semi: true,
tabWidth: 2,
singleQuote: true,
jsxSingleQuote: true,
bracketSpacing: true,
tsxSingleQuote: true,
tsSingleQuote: true,
printWidth: 80,
trailingComma: 'all',
}],
},
settings: {
react: {
version: 'detect', // Automatically detect React version
},
},
},
{ // Override for test files
files: ['src/test/**/*.{ts,tsx}'],
plugins: {
'jest-dom': jestDom,
'testing-library': pluginTestingLibrary,
},
rules: {
...jestDom.configs.recommended.rules,
...pluginTestingLibrary.configs.react.rules,
'react/display-name': 'off',
'react/prop-types': 'off',
'testing-library/no-debugging-utils': 'error'
},
},
{ // Override for Playwright tests
files: ['playwright/**/*.ts'],
plugins: {
playwright: pluginPlaywright,
},
rules: {
...pluginPlaywright.configs.recommended.rules,
'playwright/no-conditional-in-test': 'off',
'playwright/no-conditional-expect': 'off',
'playwright/no-skipped-test': [
'error',
{
'allowConditional': true
}
]
},
},
]);

View file

@ -1,150 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const { sentryWebpackPlugin } = require('@sentry/webpack-plugin');
const webpack = require('webpack');
const plugins = [];
function add_define(key, value) {
const definePluginIndex = plugins.findIndex(
(plugin) => plugin instanceof webpack.DefinePlugin
);
if (definePluginIndex !== -1) {
const definePlugin = plugins[definePluginIndex];
const newDefinePlugin = new webpack.DefinePlugin({
...definePlugin.definitions,
[key]: JSON.stringify(value),
});
plugins[definePluginIndex] = newDefinePlugin;
} else {
plugins.push(
new webpack.DefinePlugin({
[key]: JSON.stringify(value),
})
);
}
}
if (process.env.MSW) {
// Copy mockServiceWorker.js to ./dist/ so it is served with the bundle
plugins.push(
new CopyPlugin({
patterns: [
{ from: 'src/mockServiceWorker.js', to: 'mockServiceWorker.js' },
],
})
);
/*
We would like the client to be able to determine whether or not to start
the service worker at run time based on the value of process.env.MSW. We can
add that variable to process.env via the DefinesPlugin plugin, but
DefinePlugin has already been added by config() to the default webpackConfig.
Therefore, we find it in the `plugins` array based on its type, then update
it to add our new process.env.MSW variable.
*/
add_define('process.env.MSW', process.env.MSW);
}
if (process.env.NODE_ENV) {
add_define('process.env.NODE_ENV', process.env.NODE_ENV);
}
if (process.env.ENABLE_SENTRY) {
plugins.push(
sentryWebpackPlugin({
...(process.env.SENTRY_AUTH_TOKEN && {
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
org: 'red-hat-it',
project: 'image-builder-rhel',
moduleMetadata: ({ release }) => ({
dsn: 'https://f4b4288bbb7cf6c0b2ac1a2b90a076bf@o490301.ingest.us.sentry.io/4508297557901312',
org: 'red-hat-it',
project: 'image-builder-rhel',
release,
}),
})
);
}
module.exports = {
sassPrefix: '.imageBuilder',
debug: true,
useFileHash: true,
/*
mockServiceWorker.js will be served from /beta/apps/image-builder, which
will become its default scope. Setting the Service-Worker-Allowed header to
'/' allows the worker's scope to be expanded to the root route '/'.
The default webpackConfig for stage does not contain any headers.
Caution: The default webpackConfig for prod *does* contain headers, so this
code will need to be modified if using MSW in prod-beta or prod-stable so that
those headers are not overwritten.
*/
devServer: process.env.MSW && {
headers: { 'Service-Worker-Allowed': '/' },
},
devtool: 'hidden-source-map',
appUrl: '/insights/image-builder',
useProxy: true,
useAgent: true,
bounceProd: false,
proxyVerbose: true,
resolve: {
alias: {
// we don't wan't these packages bundled with
// the service frontend, so we can set the aliases
// to false
cockpit: false,
'cockpit/fsinfo': false,
'os-release': false,
},
},
module: {
rules: [
{
// running `make` on cockpit plugin creates './pkg'
// directory, the generated files do not pass
// `npm run build` outputing failures
// this ensures the directory is exluded during build time
exclude: ',/pkg',
},
],
},
routes: {
...(process.env.CONFIG_PORT && {
[`${process.env.BETA ? '/beta' : ''}/config`]: {
host: `http://localhost:${process.env.CONFIG_PORT}`,
},
}),
...(process.env.LOCAL_API && {
...(process.env.LOCAL_API.split(',') || []).reduce((acc, curr) => {
const [appName, appConfig] = (curr || '').split(':');
const [appPort = 8003, protocol = 'http', host = 'localhost'] =
appConfig.split('~');
return {
...acc,
[`/apps/${appName}`]: { host: `${protocol}://${host}:${appPort}` },
[`/beta/apps/${appName}`]: {
host: `${protocol}://${host}:${appPort}`,
},
};
}, {}),
}),
},
plugins: plugins,
moduleFederation: {
exposes: {
'./RootApp': path.resolve(__dirname, './src/AppEntry.tsx'),
},
shared: [{ 'react-router-dom': { singleton: true, version: '*' } }],
exclude: ['react-router-dom'],
},
};

11
jest.setup.js Normal file
View file

@ -0,0 +1,11 @@
import 'whatwg-fetch';
import { server } from './src/test/mocks/server';
jest.mock('@unleash/proxy-client-react', () => ({
useUnleashContext: () => jest.fn(),
useFlag: jest.fn(() => true),
}));
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

33857
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,121 +7,121 @@
"npm": ">=7.0.0"
},
"dependencies": {
"@ltd/j-toml": "1.38.0",
"@patternfly/patternfly": "6.3.1",
"@patternfly/react-code-editor": "6.3.1",
"@patternfly/react-core": "6.3.1",
"@patternfly/react-table": "6.3.1",
"@redhat-cloud-services/frontend-components": "7.0.3",
"@redhat-cloud-services/frontend-components-notifications": "6.1.5",
"@redhat-cloud-services/frontend-components-utilities": "7.0.3",
"@redhat-cloud-services/types": "3.0.1",
"@reduxjs/toolkit": "2.8.2",
"@scalprum/react-core": "0.9.5",
"@sentry/webpack-plugin": "4.1.1",
"@unleash/proxy-client-react": "5.0.1",
"classnames": "2.5.1",
"jwt-decode": "4.0.0",
"@data-driven-forms/pf4-component-mapper": "3.20.13",
"@data-driven-forms/react-form-renderer": "3.21.7",
"@patternfly/patternfly": "4.224.2",
"@patternfly/react-core": "4.276.8",
"@patternfly/react-table": "4.113.3",
"@redhat-cloud-services/frontend-components": "3.11.2",
"@redhat-cloud-services/frontend-components-notifications": "3.2.14",
"@redhat-cloud-services/frontend-components-utilities": "3.7.4",
"@reduxjs/toolkit": "^1.9.5",
"@scalprum/react-core": "^0.5.1",
"@unleash/proxy-client-react": "^3.6.0",
"classnames": "2.3.2",
"lodash": "4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "9.2.0",
"react-router-dom": "6.27.0",
"redux": "5.0.1",
"redux-promise-middleware": "6.2.0"
"react": "18.2.0",
"react-dom": "18.2.0",
"react-redux": "8.1.2",
"react-router-dom": "6.16.0",
"redux": "4.2.1",
"redux-promise-middleware": "6.1.3"
},
"jest": {
"coverageDirectory": "./coverage/",
"collectCoverage": true,
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/stories/*",
"!src/entry-dev.js"
],
"testEnvironment": "jsdom",
"roots": [
"<rootDir>/src/"
],
"moduleNameMapper": {
"\\.(css|scss)$": "identity-obj-proxy"
},
"transformIgnorePatterns": [
"node_modules/(?!(@scalprum|@openshift|lodash-es|uuid)/)"
],
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"./src/test/jest.setup.js"
],
"testTimeout": 10000
},
"devDependencies": {
"@babel/core": "7.28.0",
"@babel/preset-env": "7.28.0",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@currents/playwright": "1.15.3",
"@eslint/js": "9.32.0",
"@patternfly/react-icons": "6.3.1",
"@playwright/test": "1.51.1",
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "3.0.0",
"@redhat-cloud-services/frontend-components-config": "6.3.8",
"@redhat-cloud-services/tsc-transform-imports": "1.0.25",
"@rtk-query/codegen-openapi": "2.0.0",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.6.4",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/node": "24.3.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@types/react-redux": "7.1.34",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@vitejs/plugin-react": "4.7.0",
"@vitest/coverage-v8": "3.2.4",
"babel-loader": "10.0.0",
"chart.js": "4.5.0",
"chartjs-adapter-moment": "1.0.1",
"chartjs-plugin-annotation": "3.1.0",
"copy-webpack-plugin": "13.0.0",
"css-loader": "7.1.2",
"eslint": "9.33.0",
"eslint-plugin-disable-autofix": "5.0.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.2.2",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-redux": "4.2.2",
"eslint-plugin-testing-library": "7.6.6",
"@babel/core": "7.22.10",
"@babel/eslint-parser": "^7.22.9",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-transform-runtime": "7.22.10",
"@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "2.0.3",
"@redhat-cloud-services/frontend-components-config": "5.0.5",
"@rtk-query/codegen-openapi": "^1.0.0",
"@testing-library/dom": "9.3.1",
"@testing-library/jest-dom": "6.1.3",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/jest": "^29.5.3",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.2.1",
"babel-jest": "29.6.2",
"babel-plugin-dual-import": "1.2.1",
"babel-plugin-transform-imports": "2.0.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "6.8.1",
"eslint": "^8.46.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest-dom": "5.1.0",
"eslint-plugin-react": "7.33.0",
"eslint-plugin-testing-library": "5.11.1",
"git-revision-webpack-plugin": "5.0.0",
"globals": "16.3.0",
"history": "5.3.0",
"identity-obj-proxy": "3.0.0",
"jsdom": "26.1.0",
"madge": "8.0.0",
"mini-css-extract-plugin": "2.9.2",
"moment": "2.30.1",
"msw": "2.10.5",
"jest": "^29.6.2",
"jest-canvas-mock": "2.5.2",
"jest-environment-jsdom": "29.6.3",
"jest-fail-on-console": "^3.1.1",
"msw": "^1.2.3",
"npm-run-all": "4.1.5",
"path-browserify": "1.0.1",
"postcss-scss": "4.0.9",
"react-chartjs-2": "5.3.0",
"redux-mock-store": "1.5.5",
"sass": "1.90.0",
"sass-loader": "16.0.5",
"stylelint": "16.23.1",
"stylelint-config-recommended-scss": "16.0.0",
"ts-node": "10.9.2",
"ts-patch": "3.3.0",
"typescript": "5.8.3",
"typescript-eslint": "8.40.0",
"uuid": "11.1.0",
"vitest": "3.2.4",
"vitest-canvas-mock": "0.3.3",
"webpack-bundle-analyzer": "4.10.2",
"whatwg-fetch": "3.6.20"
"prop-types": "15.8.1",
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"stylelint": "15.10.3",
"stylelint-config-recommended-scss": "12.0.0",
"ts-node": "^10.9.1",
"typescript": "5.1.6",
"uuid": "9.0.0",
"webpack-bundle-analyzer": "4.9.0",
"whatwg-fetch": "^3.6.17"
},
"scripts": {
"lint": "npm-run-all lint:*",
"lint:js": "eslint src playwright",
"lint:js:fix": "eslint src playwright --fix",
"start": "fec dev",
"start:stage": "fec dev --clouddotEnv=stage",
"start:prod": "fec dev --clouddotEnv=prod",
"start:msw:stage": "NODE_ENV=development MSW=TRUE fec dev --clouddotEnv=stage",
"start:federated": "fec static",
"patch:hosts": "fec patch-etc-hosts",
"test": "TZ=UTC vitest run",
"test:watch": "TZ=UTC vitest",
"test:coverage": "TZ=UTC vitest run --coverage",
"test:cockpit": "src/test/cockpit-tests.sh",
"build": "fec build",
"build:cockpit": "webpack --config cockpit/webpack.config.ts",
"api": "bash api/codegen.sh",
"verify": "npm-run-all build lint test",
"postinstall": "ts-patch install",
"circular": "madge --circular ./src --extensions js,ts,tsx",
"circular:graph": "madge --circular ./src --extensions js,ts,tsx -i deps.png"
"lint:js": "eslint config src",
"lint:js:fix": "eslint config src --fix",
"lint:sass": "stylelint 'src/**/*.scss' --config .stylelintrc.json",
"devel": "webpack serve --config config/devel.webpack.config.js",
"prod-beta": "BETA=true PROXY=true webpack serve --config config/dev.webpack.config.js",
"prod-stable": "PROXY=true webpack serve --config config/dev.webpack.config.js",
"stage-stable": "STAGE=true npm run prod-stable",
"stage-beta": "STAGE=true npm run prod-beta",
"stage-beta:msw": "MSW=TRUE npm run stage-beta",
"test": "TZ=UTC jest --verbose --no-cache",
"test:single": "jest --verbose -w 1",
"build": "webpack --config config/prod.webpack.config.js",
"api": "bash api.sh",
"verify": "npm-run-all build lint test"
},
"insights": {
"appname": "image-builder"

View file

@ -1,66 +0,0 @@
upstream_project_url: https://github.com/osbuild/image-builder-frontend
specfile_path: cockpit/cockpit-image-builder.spec
upstream_package_name: cockpit-image-builder
downstream_package_name: cockpit-image-builder
# use the nicely formatted release description from our upstream release, instead of git shortlog
copy_upstream_release_description: true
upstream_tag_template: v{version}
actions:
create-archive:
- npm ci
- make dist
srpm_build_deps:
- make
- npm
jobs:
- job: tests
identifier: self
trigger: pull_request
tmt_plan: /plans/all/main
targets:
- centos-stream-10
- fedora-41
- fedora-42
- job: copr_build
trigger: pull_request
targets: &build_targets
- centos-stream-9
- centos-stream-9-aarch64
- centos-stream-10
- centos-stream-10-aarch64
- fedora-all
- job: copr_build
trigger: commit
branch: "^main$"
owner: "@osbuild"
project: "cockpit-image-builder-main"
preserve_project: True
targets: *build_targets
- job: copr_build
trigger: release
owner: "@osbuild"
project: "cockpit-image-builder"
preserve_project: True
targets: *build_targets
actions:
create-archive:
- npm ci
- make dist
- job: propose_downstream
trigger: release
dist_git_branches:
- fedora-42
- fedora-rawhide
- job: koji_build
trigger: commit
dist_git_branches:
- fedora-42
- fedora-rawhide

View file

@ -1,14 +0,0 @@
summary: cockpit-image-builder playwright tests
prepare:
how: install
package:
- cockpit-image-builder
discover:
how: fmf
execute:
how: tmt
/main:
summary: playwright tests
discover+:
test: /schutzbot/playwright

View file

@ -1,47 +0,0 @@
import {
defineConfig,
devices,
type ReporterDescription,
} from '@playwright/test';
import 'dotenv/config';
const reporters: ReporterDescription[] = [['html'], ['list']];
if (process.env.CURRENTS_PROJECT_ID && process.env.CURRENTS_RECORD_KEY) {
reporters.push(['@currents/playwright']);
}
export default defineConfig({
testDir: 'playwright',
fullyParallel: true,
workers: 4,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
reporter: reporters,
globalTimeout: 29.5 * 60 * 1000, // 29.5m, Set because of codebuild, we want PW to timeout before CB to get the results.
timeout: 3 * 60 * 1000, // 3m
expect: { timeout: 50_000 }, // 50s
use: {
actionTimeout: 30_000, // 30s
navigationTimeout: 30_000, // 30s
headless: true,
baseURL: process.env.BASE_URL
? process.env.BASE_URL
: 'http://127.0.0.1:9090',
video: 'retain-on-failure',
trace: 'on',
ignoreHTTPSErrors: true,
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json',
},
dependencies: ['setup'],
},
],
});

View file

@ -1,214 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
const validCallbackUrl =
'https://controller.url/api/controller/v2/job_templates/9/callback/';
const validHttpCallbackUrl =
'http://controller.url/api/controller/v2/job_templates/9/callback/';
const validHostConfigKey = 'hostconfigkey';
const validCertificate = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
-----END CERTIFICATE-----`;
const invalidCertificate = `-----BEGIN CERTIFICATE-----
ThisIs*Not+Valid/Base64==
-----END CERTIFICATE-----`;
test('Create a blueprint with AAP registration customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the AAP step with valid configuration', async () => {
await frame
.getByRole('button', { name: 'Ansible Automation Platform' })
.click();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validCallbackUrl);
await frame
.getByRole('textbox', { name: 'host config key' })
.fill(validHostConfigKey);
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(validCertificate);
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
});
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
// TLS confirmation checkbox should appear for HTTPS URLs
await expect(
frame.getByRole('checkbox', {
name: 'Insecure',
}),
).toBeVisible();
// Check TLS confirmation and verify CA input is hidden
await frame
.getByRole('checkbox', {
name: 'Insecure',
})
.check();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeHidden();
await frame
.getByRole('checkbox', {
name: 'Insecure',
})
.uncheck();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeVisible();
});
await test.step('Test certificate validation', async () => {
await frame.getByRole('textbox', { name: 'File upload' }).clear();
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(invalidCertificate);
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
await frame.getByRole('textbox', { name: 'File upload' }).clear();
await frame
.getByRole('textbox', { name: 'File upload' })
.fill(validCertificate);
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
});
await test.step('Test HTTP URL behavior', async () => {
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validHttpCallbackUrl);
// TLS confirmation checkbox should NOT appear for HTTP URLs
await expect(
frame.getByRole('checkbox', {
name: 'Insecure',
}),
).toBeHidden();
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toBeVisible();
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
await frame
.getByRole('textbox', { name: 'ansible callback url' })
.fill(validCallbackUrl);
});
await test.step('Complete AAP configuration and proceed to review', async () => {
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP and verify AAP configuration persists', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
await expect(
frame.getByRole('textbox', { name: 'ansible callback url' }),
).toHaveValue(validCallbackUrl);
await expect(
frame.getByRole('textbox', { name: 'host config key' }),
).toHaveValue(validHostConfigKey);
await expect(
frame.getByRole('textbox', { name: 'File upload' }),
).toHaveValue(validCertificate);
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page
.getByRole('button', { name: 'Ansible Automation Platform' })
.click();
await expect(
page.getByRole('textbox', { name: 'ansible callback url' }),
).toHaveValue(validCallbackUrl);
await expect(
page.getByRole('textbox', { name: 'host config key' }),
).toBeEmpty();
await expect(
page.getByRole('textbox', { name: 'File upload' }),
).toHaveValue(validCertificate);
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,230 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { FILE_SYSTEM_CUSTOMIZATION_URL } from '../../src/constants';
import { test } from '../fixtures/cleanup';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Filesystem customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Login, navigate to IB and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Check URLs for documentation', async () => {
await frame
.getByRole('button', { name: 'File system configuration' })
.click();
await frame
.getByRole('radio', { name: 'Use automatic partitioning' })
.click();
const [newPageAutomatic] = await Promise.all([
page.context().waitForEvent('page'),
frame
.getByRole('link', {
name: 'Customizing file systems during the image creation',
})
.click(),
]);
await newPageAutomatic.waitForLoadState();
const finalUrlAutomatic = newPageAutomatic.url();
expect(finalUrlAutomatic).toContain(FILE_SYSTEM_CUSTOMIZATION_URL);
await newPageAutomatic.close();
await frame
.getByRole('radio', { name: 'Manually configure partitions' })
.click();
const [newPageManual] = await Promise.all([
page.context().waitForEvent('page'),
frame
.getByRole('link', {
name: 'Read more about manual configuration here',
})
.click(),
]);
await newPageManual.waitForLoadState();
const finalUrlManual = newPageManual.url();
expect(finalUrlManual).toContain(FILE_SYSTEM_CUSTOMIZATION_URL);
await newPageManual.close();
});
await test.step('Fill manually selected partitions', async () => {
await expect(frame.getByRole('button', { name: '/' })).toBeDisabled();
const closeRootButton = frame
.getByRole('row', {
name: 'Draggable row draggable button / xfs 10 GiB',
})
.getByRole('button')
.nth(3);
await expect(closeRootButton).toBeDisabled();
await frame.getByRole('button', { name: 'Add partition' }).click();
await frame.getByRole('button', { name: '/home' }).click();
await frame.getByRole('option', { name: '/tmp' }).click();
await frame
.getByRole('textbox', { name: 'mountpoint suffix' })
.fill('/usb');
await frame
.getByRole('gridcell', { name: '1', exact: true })
.getByPlaceholder('File system')
.fill('1000');
await frame.getByRole('button', { name: 'GiB' }).nth(1).click();
await frame.getByRole('option', { name: 'KiB' }).click();
const closeTmpButton = frame
.getByRole('row', {
name: 'Draggable row draggable button /tmp /usb xfs 1000 KiB',
})
.getByRole('button')
.nth(3);
await expect(closeTmpButton).toBeEnabled();
});
await test.step('Fill the BP details', async () => {
await frame.getByRole('button', { name: 'Review and finish' }).click();
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit File system configuration step').click();
const closeRootButton = frame
.getByRole('row', {
name: 'Draggable row draggable button / xfs 10 GiB',
})
.getByRole('button')
.nth(3);
await expect(closeRootButton).toBeDisabled();
const closeTmpButton = frame
.getByRole('row', {
name: 'Draggable row draggable button /tmp /usb xfs 1000 KiB',
})
.getByRole('button')
.nth(3);
await expect(closeTmpButton).toBeEnabled();
const usbTextbox = frame.getByRole('textbox', {
name: 'mountpoint suffix',
});
await expect(usbTextbox).toHaveValue('/usb');
await frame
.getByRole('gridcell', { name: '1000', exact: true })
.getByPlaceholder('File system')
.click();
await frame
.getByRole('gridcell', { name: '1000', exact: true })
.getByPlaceholder('File system')
.fill('1024');
await frame.getByRole('button', { name: '/tmp' }).click();
await frame.getByRole('option', { name: '/usr' }).click();
await expect(
frame.getByText(
'Sub-directories for the /usr mount point are no longer supported',
),
).toBeVisible();
await frame.getByRole('button', { name: '/usr' }).click();
await frame.getByRole('option', { name: '/srv' }).click();
await frame
.getByRole('textbox', { name: 'mountpoint suffix' })
.fill('/data');
await frame.getByRole('button', { name: 'KiB' }).click();
await frame.getByRole('option', { name: 'MiB' }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await frame
.getByRole('button', { name: 'File system configuration' })
.click();
const closeRootButton = frame
.getByRole('row', {
name: 'Draggable row draggable button / xfs 10 GiB',
})
.getByRole('button')
.nth(3);
await expect(closeRootButton).toBeDisabled();
const closeTmpButton = frame
.getByRole('row', {
name: 'Draggable row draggable button /srv /data xfs 1 GiB',
})
.getByRole('button')
.nth(3);
await expect(closeTmpButton).toBeEnabled();
const dataTextbox = frame.getByRole('textbox', {
name: 'mountpoint suffix',
});
await expect(dataTextbox).toHaveValue('/data');
const size = frame
.getByRole('gridcell', { name: '1', exact: true })
.getByPlaceholder('File system');
await expect(size).toHaveValue('1');
const unitButton = frame.getByRole('button', { name: 'GiB' }).nth(1);
await expect(unitButton).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,155 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Firewall customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and correctly fill the ports in Firewall step', async () => {
await frame.getByRole('button', { name: 'Firewall' }).click();
await frame.getByPlaceholder('Add ports').fill('80:tcp');
await frame.getByRole('button', { name: 'Add ports' }).click();
await expect(frame.getByText('80:tcp')).toBeVisible();
});
await test.step('Select and correctly fill the disabled services in Firewall step', async () => {
await frame
.getByPlaceholder('Add disabled service')
.fill('disabled_service');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await expect(frame.getByText('disabled_service')).toBeVisible();
});
await test.step('Select and correctly fill the enabled services in Firewall step', async () => {
await frame.getByPlaceholder('Add enabled service').fill('enabled_service');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await expect(frame.getByText('enabled_service')).toBeVisible();
});
await test.step('Select and incorrectly fill the ports in Firewall step', async () => {
await frame.getByPlaceholder('Add ports').fill('x');
await frame.getByRole('button', { name: 'Add ports' }).click();
await expect(
frame
.getByText(
'Expected format: <port/port-name>:<protocol>. Example: 8080:tcp, ssh:tcp',
)
.nth(0),
).toBeVisible();
});
await test.step('Select and incorrectly fill the disabled services in Firewall step', async () => {
await frame.getByPlaceholder('Add disabled service').fill('1');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await expect(
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
).toBeVisible();
});
await test.step('Select and incorrectly fill the enabled services in Firewall step', async () => {
await frame.getByPlaceholder('Add enabled service').fill('ťčš');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await expect(
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
).toBeVisible();
});
await test.step('Fill the BP details', async () => {
await frame.getByRole('button', { name: 'Review and finish' }).click();
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Firewall step').click();
await frame.getByPlaceholder('Add ports').fill('90:tcp');
await frame.getByRole('button', { name: 'Add ports' }).click();
await frame.getByPlaceholder('Add disabled service').fill('x');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await frame.getByPlaceholder('Add enabled service').fill('y');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await frame.getByRole('button', { name: 'Close 80:tcp' }).click();
await frame.getByRole('button', { name: 'Close enabled_service' }).click();
await frame.getByRole('button', { name: 'Close disabled_service' }).click();
await expect(frame.getByText('90:tcp')).toBeVisible();
await expect(frame.getByText('x').nth(0)).toBeVisible();
await expect(frame.getByText('y').nth(0)).toBeVisible();
await expect(frame.getByText('80:tcp')).toBeHidden();
await expect(frame.getByText('disabled_service')).toBeHidden();
await expect(frame.getByText('enabled_service')).toBeHidden();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page.getByRole('button', { name: 'Firewall' }).click();
await expect(frame.getByText('90:tcp')).toBeVisible();
await expect(frame.getByText('x').nth(0)).toBeVisible();
await expect(frame.getByText('y').nth(0)).toBeVisible();
await expect(frame.getByText('80:tcp')).toBeHidden();
await expect(frame.getByText('disabled_service')).toBeHidden();
await expect(frame.getByText('enabled_service')).toBeHidden();
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,90 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Hostname customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
const hostname = 'testsystem';
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the Hostname step', async () => {
await frame.getByRole('button', { name: 'Hostname' }).click();
await frame.getByRole('textbox', { name: 'hostname input' }).fill(hostname);
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Hostname step').click();
await frame.getByRole('textbox', { name: 'hostname input' }).click();
await frame
.getByRole('textbox', { name: 'hostname input' })
.fill(hostname + 'edited');
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page.getByRole('button', { name: 'Hostname' }).click();
await expect(
page.getByRole('textbox', { name: 'hostname input' }),
).toHaveValue(hostname + 'edited');
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,133 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Kernel customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the Kernel step', async () => {
await frame.getByRole('button', { name: 'Kernel' }).click();
await frame.getByRole('button', { name: 'Menu toggle' }).click();
await frame.getByRole('option', { name: 'kernel', exact: true }).click();
await frame.getByPlaceholder('Add kernel argument').fill('rootwait');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await frame
.getByPlaceholder('Add kernel argument')
.fill('invalid$argument');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await expect(
frame.getByText(
'Expected format: <kernel-argument>. Example: console=tty0',
),
).toBeVisible();
await frame.getByPlaceholder('Select kernel package').fill('new-package');
await frame
.getByRole('option', { name: 'Custom kernel package "new-' })
.click();
await expect(
frame.getByRole('heading', { name: 'Warning alert: Custom kernel' }),
).toBeVisible();
await frame.getByRole('button', { name: 'Clear input' }).first().click();
await frame.getByRole('button', { name: 'Menu toggle' }).click();
await expect(
frame.getByRole('option', { name: 'new-package' }),
).toBeVisible();
await frame.getByPlaceholder('Select kernel package').fill('f');
await expect(
frame.getByRole('option', {
name: '"f" is not a valid kernel package name',
}),
).toBeVisible();
await frame.getByPlaceholder('Add kernel argument').fill('console=tty0');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await frame.getByPlaceholder('Add kernel argument').fill('xxnosmp');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await frame
.getByPlaceholder('Add kernel argument')
.fill('console=ttyS0,115200n8');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Kernel step').click();
await frame.getByRole('button', { name: 'Menu toggle' }).click();
await frame.getByRole('option', { name: 'kernel', exact: true }).click();
await frame.getByPlaceholder('Add kernel argument').fill('new=argument');
await frame.getByRole('button', { name: 'Add kernel argument' }).click();
await frame.getByRole('button', { name: 'Close xxnosmp' }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(frame);
await frame.getByRole('button', { name: 'Kernel' }).click();
await expect(frame.getByPlaceholder('Select kernel package')).toHaveValue(
'kernel',
);
await expect(frame.getByText('rootwait')).toBeVisible();
await expect(frame.getByText('console=tty0')).toBeVisible();
await expect(frame.getByText('console=ttyS0,115200n8')).toBeVisible();
await expect(frame.getByText('new=argument')).toBeVisible();
await expect(frame.getByText('xxnosmp')).toBeHidden();
await frame.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,159 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Locale customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the Locale step', async () => {
await frame.getByRole('button', { name: 'Locale' }).click();
await frame.getByPlaceholder('Select a language').fill('fy');
await frame
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
.click();
await expect(
frame.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
}),
).toBeEnabled();
await frame
.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
})
.click();
await expect(
frame.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
}),
).toBeHidden();
await frame.getByPlaceholder('Select a language').fill('fy');
await frame
.getByRole('option', { name: 'Western Frisian - Germany (fy_DE.UTF-8)' })
.click();
await expect(
frame.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
}),
).toBeEnabled();
await frame.getByPlaceholder('Select a language').fill('aa');
await frame
.getByRole('option', { name: 'aa - Djibouti (aa_DJ.UTF-8)' })
.click();
await expect(
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
).toBeEnabled();
await frame.getByPlaceholder('Select a language').fill('aa');
await expect(
frame.getByText(
'aa - Djibouti (aa_DJ.UTF-8)Language already addedaa - Eritrea (aa_ER.UTF-8)aa - Ethiopia (aa_ET.UTF-8)',
),
).toBeAttached();
await frame.getByPlaceholder('Select a language').fill('xxx');
await expect(frame.getByText('No results found for')).toBeAttached();
await frame.getByRole('button', { name: 'Menu toggle' }).nth(1).click();
await frame.getByPlaceholder('Select a keyboard').fill('ami');
await frame.getByRole('option', { name: 'amiga-de' }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Locale step').click();
await expect(
frame.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
}),
).toBeEnabled();
await expect(
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
).toBeEnabled();
await frame.getByPlaceholder('Select a language').fill('aa');
await frame
.getByRole('option', { name: 'aa - Eritrea (aa_ER.UTF-8)' })
.click();
await expect(
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
).toBeEnabled();
await frame.getByRole('button', { name: 'Clear input' }).click();
await frame.getByRole('button', { name: 'Menu toggle' }).nth(1).click();
await frame.getByRole('option', { name: 'ANSI-dvorak' }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page.getByRole('button', { name: 'Locale' }).click();
await expect(
frame.getByRole('button', {
name: 'Close Western Frisian - Germany (fy_DE.UTF-8)',
}),
).toBeEnabled();
await expect(
frame.getByRole('button', { name: 'Close aa - Djibouti (aa_DJ.UTF-8)' }),
).toBeEnabled();
await expect(
frame.getByRole('button', { name: 'Close aa - Eritrea (aa_ER.UTF-8)' }),
).toBeEnabled();
await expect(frame.getByPlaceholder('Select a keyboard')).toHaveValue(
'ANSI-dvorak',
);
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,189 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/cleanup';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import { ibFrame, navigateToLandingPage } from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with OpenSCAP customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
test.skip(!isHosted(), 'Exporting is not available in the plugin');
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Select RHEL 9 and go to optional steps in Wizard', async () => {
await frame.getByRole('button', { name: 'Create image blueprint' }).click();
await frame.getByTestId('release_select').click();
await frame
.getByRole('option', {
name: 'Red Hat Enterprise Linux (RHEL) 9 Full support ends: May 2027 | Maintenance',
})
.click();
await frame.getByRole('checkbox', { name: 'Virtualization' }).click();
await frame.getByRole('button', { name: 'Next' }).click();
await registerLater(frame);
});
await test.step('Select only OpenSCAP, and check if dependencies are preselected', async () => {
await frame.getByRole('button', { name: 'Compliance' }).click();
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
await frame
.getByRole('option', {
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server This profile',
})
.click();
await frame
.getByRole('button', { name: 'File system configuration' })
.click();
await expect(
frame
.getByRole('row', {
name: 'Draggable row draggable button /tmp xfs 1 GiB',
})
.getByRole('button')
.nth(3),
).toBeVisible();
await frame.getByRole('button', { name: 'Additional packages' }).click();
await frame.getByRole('button', { name: 'Selected (8)' }).click();
await expect(frame.getByRole('gridcell', { name: 'aide' })).toBeVisible();
await expect(frame.getByRole('gridcell', { name: 'chrony' })).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'firewalld' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'libpwquality' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'libselinux' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'nftables' }),
).toBeVisible();
await expect(frame.getByRole('gridcell', { name: 'sudo' })).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'systemd-journal-remote' }),
).toBeVisible();
await frame.getByRole('button', { name: 'Systemd services' }).click();
await expect(
frame.getByText('Required by OpenSCAPcrondfirewalldsystemd-journald'),
).toBeVisible();
await frame.getByPlaceholder('Add masked service').fill('nftables');
await frame.getByPlaceholder('Add masked service').press('Enter');
await expect(
frame.getByText('Masked service already exists'),
).toBeVisible();
await expect(frame.getByText('Required by OpenSCAPcupsnfs-')).toBeVisible();
await expect(frame.getByText('nfs-server')).toBeVisible();
await expect(frame.getByText('rpcbind')).toBeVisible();
await expect(frame.getByText('avahi-daemon')).toBeVisible();
await expect(frame.getByText('autofs')).toBeVisible();
await expect(frame.getByText('bluetooth')).toBeVisible();
await expect(frame.getByText('nftables')).toBeVisible();
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByRole('button', { name: 'Compliance' }).click();
await expect(frame.getByText('Level 1 - Server')).toBeVisible();
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
await frame
.getByRole('option', {
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 2 - Server This profile',
})
.click();
await frame.getByRole('button', { name: 'Kernel' }).click();
await expect(
frame.getByText('Required by OpenSCAPaudit_backlog_limit=8192audit='),
).toBeVisible();
await frame.getByRole('button', { name: 'Additional packages' }).click();
await frame.getByRole('button', { name: 'Selected (10)' }).click();
await expect(frame.getByRole('gridcell', { name: 'aide' })).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'audit-libs' }),
).toBeVisible();
await expect(frame.getByRole('gridcell', { name: 'chrony' })).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'firewalld' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'libpwquality' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'libselinux' }),
).toBeVisible();
await expect(
frame.getByRole('gridcell', { name: 'nftables' }),
).toBeVisible();
await expect(frame.getByRole('gridcell', { name: 'sudo' })).toBeVisible();
await frame.getByRole('button', { name: 'Systemd services' }).click();
await expect(
frame.getByText(
'Required by OpenSCAPauditdcrondfirewalldsystemd-journald',
),
).toBeVisible();
await frame.getByPlaceholder('Add masked service').fill('nftables');
await frame.getByPlaceholder('Add masked service').press('Enter');
await expect(
frame.getByText('Masked service already exists'),
).toBeVisible();
await expect(frame.getByText('Required by OpenSCAPcupsnfs-')).toBeVisible();
await expect(frame.getByText('nfs-server')).toBeVisible();
await expect(frame.getByText('rpcbind')).toBeVisible();
await expect(frame.getByText('avahi-daemon')).toBeVisible();
await expect(frame.getByText('autofs')).toBeVisible();
await expect(frame.getByText('bluetooth')).toBeVisible();
await expect(frame.getByText('nftables')).toBeVisible();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async () => {
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async () => {
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async () => {
await fillInImageOutputGuest(page);
await page.getByRole('button', { name: 'Compliance' }).click();
await expect(frame.getByText('Level 2 - Server')).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,156 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Systemd customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and correctly fill all of the service fields', async () => {
await frame.getByRole('button', { name: 'Systemd services' }).click();
await frame
.getByPlaceholder('Add disabled service')
.fill('systemd-dis.service');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await expect(frame.getByText('systemd-dis.service')).toBeVisible();
await frame
.getByPlaceholder('Add enabled service')
.fill('systemd-en.service');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await expect(frame.getByText('systemd-en.service')).toBeVisible();
await frame
.getByPlaceholder('Add masked service')
.fill('systemd-m.service');
await frame.getByRole('button', { name: 'Add masked service' }).click();
await expect(frame.getByText('systemd-m.service')).toBeVisible();
});
await test.step('Select and incorrectly fill all of the service fields', async () => {
await frame.getByPlaceholder('Add disabled service').fill('&&');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await expect(
frame.getByText('Expected format: <service-name>. Example: sshd').nth(0),
).toBeVisible();
await frame.getByPlaceholder('Add enabled service').fill('áá');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await expect(
frame.getByText('Expected format: <service-name>. Example: sshd').nth(1),
).toBeVisible();
await frame.getByPlaceholder('Add masked service').fill('78');
await frame.getByRole('button', { name: 'Add masked service' }).click();
await expect(
frame.getByText('Expected format: <service-name>. Example: sshd').nth(2),
).toBeVisible();
});
await test.step('Fill the BP details', async () => {
await frame.getByRole('button', { name: 'Review and finish' }).click();
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Systemd services step').click();
await frame
.getByPlaceholder('Add disabled service')
.fill('disabled-service');
await frame.getByRole('button', { name: 'Add disabled service' }).click();
await frame.getByPlaceholder('Add enabled service').fill('enabled-service');
await frame.getByRole('button', { name: 'Add enabled service' }).click();
await frame.getByPlaceholder('Add masked service').fill('masked-service');
await frame.getByRole('button', { name: 'Add masked service' }).click();
await frame
.getByRole('button', { name: 'Close systemd-m.service' })
.click();
await frame
.getByRole('button', { name: 'Close systemd-en.service' })
.click();
await frame
.getByRole('button', { name: 'Close systemd-dis.service' })
.click();
await expect(frame.getByText('enabled-service')).toBeVisible();
await expect(frame.getByText('disabled-service')).toBeVisible();
await expect(frame.getByText('masked-service')).toBeVisible();
await expect(frame.getByText('systemd-en.service')).toBeHidden();
await expect(frame.getByText('systemd-dis.service')).toBeHidden();
await expect(frame.getByText('systemd-m.service')).toBeHidden();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await page.getByRole('button', { name: 'Systemd services' }).click();
await expect(frame.getByText('enabled-service')).toBeVisible();
await expect(frame.getByText('disabled-service')).toBeVisible();
await expect(frame.getByText('masked-service')).toBeVisible();
await expect(frame.getByText('systemd-en.service')).toBeHidden();
await expect(frame.getByText('systemd-dis.service')).toBeHidden();
await expect(frame.getByText('systemd-m.service')).toBeHidden();
await page.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,128 +0,0 @@
import { expect } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { test } from '../fixtures/customizations';
import { isHosted } from '../helpers/helpers';
import { ensureAuthenticated } from '../helpers/login';
import {
ibFrame,
navigateToLandingPage,
navigateToOptionalSteps,
} from '../helpers/navHelpers';
import {
createBlueprint,
deleteBlueprint,
exportBlueprint,
fillInDetails,
fillInImageOutputGuest,
importBlueprint,
registerLater,
} from '../helpers/wizardHelpers';
test('Create a blueprint with Timezone customization', async ({
page,
cleanup,
}) => {
const blueprintName = 'test-' + uuidv4();
// Delete the blueprint after the run fixture
await cleanup.add(() => deleteBlueprint(page, blueprintName));
await ensureAuthenticated(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await test.step('Navigate to optional steps in Wizard', async () => {
await navigateToOptionalSteps(frame);
await registerLater(frame);
});
await test.step('Select and fill the Timezone step', async () => {
await frame.getByRole('button', { name: 'Timezone' }).click();
await frame.getByPlaceholder('Select a timezone').fill('Canada');
await frame.getByRole('option', { name: 'Canada/Saskatchewan' }).click();
await frame.getByRole('button', { name: 'Clear input' }).first().click();
await frame.getByPlaceholder('Select a timezone').fill('Europe');
await frame.getByRole('option', { name: 'Europe/Stockholm' }).click();
await frame.getByPlaceholder('Add NTP servers').fill('0.nl.pool.ntp.org');
await frame.getByRole('button', { name: 'Add NTP server' }).click();
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
await frame.getByPlaceholder('Add NTP servers').fill('0.nl.pool.ntp.org');
await frame.getByRole('button', { name: 'Add NTP server' }).click();
await expect(frame.getByText('NTP server already exists.')).toBeVisible();
await frame.getByPlaceholder('Add NTP servers').fill('xxxx');
await frame.getByRole('button', { name: 'Add NTP server' }).click();
await expect(
frame
.getByText('Expected format: <ntp-server>. Example: time.redhat.com')
.nth(0),
).toBeVisible();
await frame.getByPlaceholder('Add NTP servers').fill('0.cz.pool.ntp.org');
await frame.getByRole('button', { name: 'Add NTP server' }).click();
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeVisible();
await frame.getByPlaceholder('Add NTP servers').fill('0.de.pool.ntp.org');
await frame.getByRole('button', { name: 'Add NTP server' }).click();
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
await frame
.getByRole('button', { name: 'Close 0.cz.pool.ntp.org' })
.click();
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeHidden();
await frame.getByRole('button', { name: 'Review and finish' }).click();
});
await test.step('Fill the BP details', async () => {
await fillInDetails(frame, blueprintName);
});
await test.step('Create BP', async () => {
await createBlueprint(frame, blueprintName);
});
await test.step('Edit BP', async () => {
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByLabel('Revisit Timezone step').click();
await expect(frame.getByText('Canada/Saskatchewan')).toBeHidden();
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
'Europe/Stockholm',
);
await frame.getByPlaceholder('Select a timezone').fill('Europe');
await frame.getByRole('option', { name: 'Europe/Oslo' }).click();
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
'Europe/Oslo',
);
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeHidden();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
});
// This is for hosted service only as these features are not available in cockpit plugin
await test.step('Export BP', async (step) => {
step.skip(!isHosted(), 'Exporting is not available in the plugin');
await exportBlueprint(page, blueprintName);
});
await test.step('Import BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await importBlueprint(page, blueprintName);
});
await test.step('Review imported BP', async (step) => {
step.skip(!isHosted(), 'Importing is not available in the plugin');
await fillInImageOutputGuest(page);
await frame.getByRole('button', { name: 'Timezone' }).click();
await expect(frame.getByPlaceholder('Select a timezone')).toHaveValue(
'Europe/Oslo',
);
await expect(frame.getByText('0.nl.pool.ntp.org')).toBeVisible();
await expect(frame.getByText('0.de.pool.ntp.org')).toBeVisible();
await expect(frame.getByText('0.cz.pool.ntp.org')).toBeHidden();
await frame.getByRole('button', { name: 'Cancel' }).click();
});
});

View file

@ -1,44 +0,0 @@
import { test as oldTest } from '@playwright/test';
type WithCleanup = {
cleanup: Cleanup;
};
export interface Cleanup {
add: (cleanupFn: () => Promise<unknown>) => symbol;
runAndAdd: (cleanupFn: () => Promise<unknown>) => Promise<symbol>;
remove: (key: symbol) => void;
}
export const test = oldTest.extend<WithCleanup>({
cleanup: async ({}, use) => {
const cleanupFns: Map<symbol, () => Promise<unknown>> = new Map();
// eslint-disable-next-line react-hooks/rules-of-hooks
await use({
add: (cleanupFn) => {
const key = Symbol();
cleanupFns.set(key, cleanupFn);
return key;
},
runAndAdd: async (cleanupFn) => {
await cleanupFn();
const key = Symbol();
cleanupFns.set(key, cleanupFn);
return key;
},
remove: (key) => {
cleanupFns.delete(key);
},
});
await test.step(
'Post-test cleanup',
async () => {
await Promise.all(Array.from(cleanupFns).map(([, fn]) => fn()));
},
{ box: true },
);
},
});

View file

@ -1,8 +0,0 @@
// This is a common fixture for the customizations tests
import { mergeTests } from '@playwright/test';
import { test as cleanupTest } from './cleanup';
import { test as popupTest } from './popupHandler';
// Combine the fixtures into one
export const test = mergeTests(cleanupTest, popupTest);

View file

@ -1,18 +0,0 @@
import { test as base } from '@playwright/test';
import { closePopupsIfExist } from '../helpers/helpers';
export interface PopupHandlerFixture {
popupHandler: void;
}
// This fixture will close any popups that might get opened during the test execution
export const test = base.extend<PopupHandlerFixture>({
popupHandler: [
async ({ page }, use) => {
await closePopupsIfExist(page);
await use(undefined);
},
{ auto: true },
],
});

View file

@ -1,12 +0,0 @@
import { test as setup } from '@playwright/test';
import { login, storeStorageStateAndToken } from './helpers/login';
setup.describe('Setup', () => {
setup.describe.configure({ retries: 3 });
setup('Authenticate', async ({ page }) => {
await login(page);
await storeStorageStateAndToken(page);
});
});

View file

@ -1,87 +0,0 @@
import { execSync } from 'child_process';
import { readFileSync } from 'node:fs';
import { expect, type Page } from '@playwright/test';
export const togglePreview = async (page: Page) => {
const toggleSwitch = page.locator('#preview-toggle');
if (!(await toggleSwitch.isChecked())) {
await toggleSwitch.click();
}
const turnOnButton = page.getByRole('button', { name: 'Turn on' });
if (await turnOnButton.isVisible()) {
await turnOnButton.click();
}
await expect(toggleSwitch).toBeChecked();
};
export const isHosted = (): boolean => {
return process.env.BASE_URL?.includes('redhat.com') || false;
};
export const closePopupsIfExist = async (page: Page) => {
const locatorsToCheck = [
page.locator('.pf-v6-c-alert.notification-item button'), // This closes all toast pop-ups
page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up
page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pop-up
page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.)
page
.locator('iframe[name="intercom-modal-frame"]')
.contentFrame()
.getByRole('button', { name: 'Close' }), // This closes the intercom pop-up
page
.locator('iframe[name="intercom-notifications-frame"]')
.contentFrame()
.getByRole('button', { name: 'Profile image for Rob Rob' })
.last(), // This closes the intercom pop-up notification at the bottom of the screen, the last notification is displayed first if stacked (different from the modal popup handled above)
];
for (const locator of locatorsToCheck) {
await page.addLocatorHandler(locator, async () => {
await locator.first().click({ timeout: 10_000, noWaitAfter: true }); // There can be multiple toast pop-ups
});
}
};
// copied over from constants
const ON_PREM_RELEASES = new Map([
['centos-10', 'CentOS Stream 10'],
['fedora-41', 'Fedora Linux 41'],
['fedora-42', 'Fedora Linux 42'],
['rhel-10', 'Red Hat Enterprise Linux (RHEL) 10'],
]);
/* eslint-disable @typescript-eslint/no-explicit-any */
export const getHostDistroName = (): string => {
const osRelData = readFileSync('/etc/os-release');
const lines = osRelData
.toString('utf-8')
.split('\n')
.filter((l) => l !== '');
const osRel = {};
for (const l of lines) {
const lineData = l.split('=');
(osRel as any)[lineData[0]] = lineData[1].replace(/"/g, '');
}
// strip minor version from rhel
const distro = ON_PREM_RELEASES.get(
`${(osRel as any)['ID']}-${(osRel as any)['VERSION_ID'].split('.')[0]}`,
);
if (distro === undefined) {
/* eslint-disable no-console */
console.error('getHostDistroName failed, os-release config:', osRel);
throw new Error('getHostDistroName failed, distro undefined');
}
return distro;
};
export const getHostArch = (): string => {
return execSync('uname -m').toString('utf-8').replace(/\s/g, '');
};

View file

@ -1,138 +0,0 @@
import path from 'path';
import { expect, type Page } from '@playwright/test';
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
import { ibFrame } from './navHelpers';
/**
* Logs in to either Cockpit or Console, will distinguish between them based on the environment
* @param page - the page object
*/
export const login = async (page: Page) => {
if (!process.env.PLAYWRIGHT_USER || !process.env.PLAYWRIGHT_PASSWORD) {
throw new Error('user or password not set in environment');
}
const user = process.env.PLAYWRIGHT_USER;
const password = process.env.PLAYWRIGHT_PASSWORD;
if (isHosted()) {
return loginConsole(page, user, password);
}
return loginCockpit(page, user, password);
};
/**
* Checks if the user is already authenticated, if not, logs them in
* @param page - the page object
*/
export const ensureAuthenticated = async (page: Page) => {
// Navigate to the target page
if (isHosted()) {
await page.goto('/insights/image-builder/landing');
} else {
await page.goto('/cockpit-image-builder');
}
// Check for authentication success indicator
const successIndicator = isHosted()
? page.getByRole('heading', { name: 'All images' })
: ibFrame(page).getByRole('heading', { name: 'All images' });
let isAuthenticated = false;
try {
// Give it a 30 second period to load, it's less expensive than having to rerun the test
await expect(successIndicator).toBeVisible({ timeout: 30000 });
isAuthenticated = true;
} catch {
isAuthenticated = false;
}
if (!isAuthenticated) {
// Not authenticated, need to login
await login(page);
}
};
const loginCockpit = async (page: Page, user: string, password: string) => {
await page.goto('/cockpit-image-builder');
await page.getByRole('textbox', { name: 'User name' }).fill(user);
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('button', { name: 'Log in' }).click();
// image-builder lives inside an iframe
const frame = ibFrame(page);
try {
// Check if the user already has administrative access
await expect(
page.getByRole('button', { name: 'Administrative access' }),
).toBeVisible();
} catch {
// If not, try to gain it
// cockpit-image-builder needs superuser, expect an error message
// when the user does not have admin priviliges
await expect(
frame.getByRole('heading', { name: 'Access is limited' }),
).toBeVisible();
await page.getByRole('button', { name: 'Limited access' }).click();
// different popup opens based on type of account (can be passwordless)
const authenticateButton = page.getByRole('button', {
name: 'Authenticate',
});
const closeButton = page.getByText('Close');
await expect(authenticateButton.or(closeButton)).toBeVisible();
if (await authenticateButton.isVisible()) {
// with password
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await authenticateButton.click();
}
if (await closeButton.isVisible()) {
// passwordless
await closeButton.click();
}
}
// expect to have administrative access
await expect(
page.getByRole('button', { name: 'Administrative access' }),
).toBeVisible();
await expect(
frame.getByRole('heading', { name: 'All images' }),
).toBeVisible();
};
const loginConsole = async (page: Page, user: string, password: string) => {
await closePopupsIfExist(page);
await page.goto('/insights/image-builder/landing');
await page.getByRole('textbox', { name: 'Red Hat login' }).fill(user);
await page.getByRole('button', { name: 'Next' }).click();
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('button', { name: 'Log in' }).click();
await togglePreview(page);
await expect(page.getByRole('heading', { name: 'All images' })).toBeVisible();
};
export const storeStorageStateAndToken = async (page: Page) => {
const { cookies } = await page
.context()
.storageState({ path: path.join(__dirname, '../../.auth/user.json') });
if (isHosted()) {
// For hosted service, look for cs_jwt token
process.env.TOKEN = `Bearer ${
cookies.find((cookie) => cookie.name === 'cs_jwt')?.value
}`;
} else {
// For Cockpit, we don't need a TOKEN but we can still store it for consistency
const cockpitCookie = cookies.find((cookie) => cookie.name === 'cockpit');
if (cockpitCookie) {
process.env.TOKEN = cockpitCookie.value;
}
}
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(100);
};

View file

@ -1,45 +0,0 @@
import { expect, FrameLocator, Page } from '@playwright/test';
import { getHostArch, getHostDistroName, isHosted } from './helpers';
/**
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
* @param page - the page object
*/
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
await page.getByRole('button', { name: 'Create image blueprint' }).click();
if (!isHosted()) {
// wait until the distro and architecture aligns with the host
await expect(page.getByTestId('release_select')).toHaveText(
getHostDistroName(),
);
await expect(page.getByTestId('arch_select')).toHaveText(getHostArch());
}
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
await page.getByRole('button', { name: 'Next' }).click();
};
/**
* Returns the FrameLocator object in case we are using cockpit plugin, else it returns the page object
* @param page - the page object
*/
export const ibFrame = (page: Page): FrameLocator | Page => {
if (isHosted()) {
return page;
}
return page
.locator('iframe[name="cockpit1\\:localhost\\/cockpit-image-builder"]')
.contentFrame();
};
/**
* Navigates to the landing page of the Image Builder
* @param page - the page object
*/
export const navigateToLandingPage = async (page: Page) => {
if (isHosted()) {
await page.goto('/insights/image-builder/landing');
} else {
await page.goto('/cockpit-image-builder');
}
};

View file

@ -1,145 +0,0 @@
import { expect, FrameLocator, type Page, test } from '@playwright/test';
import { closePopupsIfExist, isHosted } from './helpers';
import { ibFrame, navigateToLandingPage } from './navHelpers';
/**
* Clicks the create button, handles the modal, clicks the button again and selecets the BP in the list
* @param page - the page object
* @param blueprintName - the name of the created blueprint
*/
export const createBlueprint = async (
page: Page | FrameLocator,
blueprintName: string,
) => {
await page.getByRole('button', { name: 'Create blueprint' }).click();
await page.getByRole('button', { name: 'Close' }).first().click();
await page.getByRole('button', { name: 'Create blueprint' }).click();
await page.getByRole('textbox', { name: 'Search input' }).fill(blueprintName);
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await page.locator(`button[id="${blueprintName}"]`).click();
};
/**
* Fill in the "Details" step in the wizard
* This method assumes that the "Details" step is ENABLED!
* After filling the step, it will click the "Next" button
* Description defaults to "Testing blueprint"
* @param page - the page object
* @param blueprintName - the name of the blueprint to create
*/
export const fillInDetails = async (
page: Page | FrameLocator,
blueprintName: string,
) => {
await page.getByRole('listitem').filter({ hasText: 'Details' }).click();
await page
.getByRole('textbox', { name: 'Blueprint name' })
.fill(blueprintName);
await page
.getByRole('textbox', { name: 'Blueprint description' })
.fill('Testing blueprint');
await page.getByRole('button', { name: 'Next' }).click();
};
/**
* Select "Register later" option in the wizard
* This function executes only on the hosted service
* @param page - the page object
*/
export const registerLater = async (page: Page | FrameLocator) => {
if (isHosted()) {
await page.getByRole('button', { name: 'Register' }).click();
await page.getByRole('radio', { name: 'Register later' }).click();
}
};
/**
* Fill in the image output step in the wizard by selecting the Guest Image
* @param page - the page object
*/
export const fillInImageOutputGuest = async (page: Page | FrameLocator) => {
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
await page.getByRole('button', { name: 'Next' }).click();
};
/**
* Delete the blueprint with the given name
* Will locate to the Image Builder page and search for the blueprint first
* If the blueprint is not found, it will fail gracefully
* @param page - the page object
* @param blueprintName - the name of the blueprint to delete
*/
export const deleteBlueprint = async (page: Page, blueprintName: string) => {
// Since new browser is opened during the BP cleanup, we need to call the popup closer again
await closePopupsIfExist(page);
await test.step(
'Delete the blueprint with name: ' + blueprintName,
async () => {
// Locate back to the Image Builder page every time because the test can fail at any stage
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await frame
.getByRole('textbox', { name: 'Search input' })
.fill(blueprintName);
// Check if no blueprints found -> that means no blueprint was created -> fail gracefully and do not raise error
try {
await expect(
frame.getByRole('heading', { name: 'No blueprints found' }),
).toBeVisible({ timeout: 5_000 }); // Shorter timeout to avoid hanging uncessarily
return; // Fail gracefully, no blueprint to delete
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// If the No BP heading was not found, it means the blueprint (possibly) was created -> continue with deletion
}
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await frame.locator(`button[id="${blueprintName}"]`).click();
await frame.getByRole('button', { name: 'Menu toggle' }).click();
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
await frame.getByRole('button', { name: 'Delete' }).click();
},
{ box: true },
);
};
/**
* Export the blueprint
* This function executes only on the hosted service
* @param page - the page object
*/
export const exportBlueprint = async (page: Page, blueprintName: string) => {
if (isHosted()) {
await page.getByRole('button', { name: 'Menu toggle' }).click();
const downloadPromise = page.waitForEvent('download');
await page
.getByRole('menuitem', { name: 'Download blueprint (.json)' })
.click();
const download = await downloadPromise;
await download.saveAs('../../downloads/' + blueprintName + '.json');
}
};
/**
* Import the blueprint
* This function executes only on the hosted service
* @param page - the page object
*/
export const importBlueprint = async (
page: Page | FrameLocator,
blueprintName: string,
) => {
if (isHosted()) {
await page.getByRole('button', { name: 'Import' }).click();
const dragBoxSelector = page.getByRole('presentation').first();
await dragBoxSelector
.locator('input[type=file]')
.setInputFiles('../../downloads/' + blueprintName + '.json');
await expect(
page.getByRole('textbox', { name: 'File upload' }),
).not.toBeEmpty();
await page.getByRole('button', { name: 'Review and Finish' }).click();
}
};

View file

@ -1,314 +0,0 @@
import { readFileSync } from 'node:fs';
import TOML from '@ltd/j-toml';
import { expect, test } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { closePopupsIfExist, isHosted } from './helpers/helpers';
import { ensureAuthenticated } from './helpers/login';
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
test.describe.serial('test', () => {
const blueprintName = uuidv4();
test('create blueprint', async ({ page }) => {
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
frame.getByRole('heading', { name: 'Images About image builder' });
frame.getByRole('heading', { name: 'Blueprints' });
await frame.getByTestId('blueprints-create-button').click();
frame.getByRole('heading', { name: 'Image output' });
await frame
.getByRole('checkbox', { name: /Virtualization guest image/i })
.click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (isHosted()) {
frame.getByRole('heading', {
name: 'Register systems using this image',
});
await page.getByRole('radio', { name: /Register later/i }).click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
frame.getByRole('heading', { name: 'Compliance' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'File system configuration' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (isHosted()) {
frame.getByRole('heading', { name: 'Repository snapshot' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Custom repositories' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
frame.getByRole('heading', { name: 'Additional packages' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Users' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Timezone' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Locale' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Hostname' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Kernel' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Firewall' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
frame.getByRole('heading', { name: 'Systemd services' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (isHosted()) {
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
if (isHosted()) {
frame.getByRole('heading', { name: 'First boot configuration' });
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
frame.getByRole('heading', { name: 'Details' });
await frame.getByTestId('blueprint').fill(blueprintName);
await expect(frame.getByTestId('blueprint')).toHaveValue(blueprintName);
await frame.getByRole('button', { name: 'Next', exact: true }).click();
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await frame.getByTestId('close-button-saveandbuild-modal').click();
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await expect(
frame.locator('.pf-v6-c-card__title-text').getByText(
// if the name is too long, the blueprint card will have a truncated name.
blueprintName.length > 24
? blueprintName.slice(0, 24) + '...'
: blueprintName,
),
).toBeVisible();
});
test('edit blueprint', async ({ page }) => {
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// package searching is really slow the first time in cockpit
if (!isHosted()) {
test.setTimeout(300000);
}
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await frame
.getByRole('textbox', { name: 'Search input' })
.fill(blueprintName);
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await frame.locator(`button[id="${blueprintName}"]`).click();
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByRole('button', { name: 'Additional packages' }).click();
await frame
.getByTestId('packages-search-input')
.locator('input')
.fill('osbuild-composer');
frame.getByTestId('packages-table').getByText('Searching');
frame.getByRole('gridcell', { name: 'osbuild-composer' }).first();
await frame.getByRole('checkbox', { name: 'Select row 0' }).check();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame.getByRole('button', { name: 'About packages' }).click();
frame.getByRole('gridcell', { name: 'osbuild-composer' });
await frame.getByRole('button', { name: 'Close', exact: true }).click();
await frame
.getByRole('button', { name: 'Save changes to blueprint' })
.click();
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
await frame.getByRole('button', { name: 'About packages' }).click();
frame.getByRole('gridcell', { name: 'osbuild-composer' });
await frame.getByRole('button', { name: 'Close', exact: true }).click();
await frame.getByRole('button', { name: 'Cancel', exact: true }).click();
frame.getByRole('heading', { name: 'All images' });
});
test('build blueprint', async ({ page }) => {
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await frame
.getByRole('textbox', { name: 'Search input' })
.fill(blueprintName);
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await frame.locator(`button[id="${blueprintName}"]`).click();
await frame.getByTestId('blueprint-build-image-menu-option').click();
// make sure the image is present
await frame
.getByTestId('images-table')
.getByRole('button', { name: 'Details' })
.click();
frame.getByText('Build Information');
});
test('delete blueprint', async ({ page }) => {
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
const frame = await ibFrame(page);
await frame
.getByRole('textbox', { name: 'Search input' })
.fill(blueprintName);
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await frame.locator(`button[id="${blueprintName}"]`).click();
await frame.getByRole('button', { name: /blueprint menu toggle/i }).click();
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
await frame.getByRole('button', { name: 'Delete' }).click();
});
test('cockpit worker config', async ({ page }) => {
if (isHosted()) {
return;
}
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
await page.goto('/cockpit-image-builder');
const frame = ibFrame(page);
const header = frame.getByText('Configure AWS Uploads');
if (!(await header.isVisible())) {
await frame
.getByRole('button', { name: 'Configure Cloud Providers' })
.click();
await expect(header).toBeVisible();
}
const bucket = 'cockpit-ib-playwright-bucket';
const credentials = '/test/credentials';
const switchInput = frame.locator('#aws-config-switch');
await expect(switchInput).toBeVisible();
// introduce a wait time, since it takes some time to load the
// worker config file.
await page.waitForTimeout(1000);
// If this test fails for any reason, the config should already be loaded
// and visible on the retury. If it is go back to the landing page
if (await switchInput.isChecked()) {
await frame.getByRole('button', { name: 'Cancel' }).click();
await expect(
frame.getByRole('heading', { name: 'All images' }),
).toBeVisible();
} else {
const switchToggle = frame.locator('.pf-v6-c-switch');
await switchToggle.click();
await frame
.getByPlaceholder('AWS bucket')
// this doesn't need to exist, we're just testing that
// the form works as expected
.fill(bucket);
await frame.getByPlaceholder('Path to AWS credentials').fill(credentials);
await frame.getByRole('button', { name: 'Submit' }).click();
await expect(
frame.getByRole('heading', { name: 'All images' }),
).toBeVisible();
}
await frame
.getByRole('button', { name: 'Configure Cloud Providers' })
.click();
await expect(header).toBeVisible();
// introduce a wait time, since it takes some time to load the
// worker config file.
await page.waitForTimeout(1500);
await expect(frame.locator('#aws-config-switch')).toBeChecked();
await expect(frame.getByPlaceholder('AWS bucket')).toHaveValue(bucket);
await expect(frame.getByPlaceholder('Path to AWS credentials')).toHaveValue(
credentials,
);
await frame.getByRole('button', { name: 'Cancel' }).click();
const config = readFileSync('/etc/osbuild-worker/osbuild-worker.toml');
// this is for testing, the field `aws` should exist
// eslint-disable-next-line
const parsed = TOML.parse(config) as any;
expect(parsed.aws?.bucket).toBe(bucket);
expect(parsed.aws?.credentials).toBe(credentials);
});
const cockpitBlueprintname = uuidv4();
test('cockpit cloud upload', async ({ page }) => {
if (isHosted()) {
return;
}
await ensureAuthenticated(page);
await closePopupsIfExist(page);
// Navigate to IB landing page and get the frame
await navigateToLandingPage(page);
await page.goto('/cockpit-image-builder');
const frame = ibFrame(page);
frame.getByRole('heading', { name: 'Images About image builder' });
frame.getByRole('heading', { name: 'Blueprints' });
await frame.getByTestId('blueprints-create-button').click();
frame.getByRole('heading', { name: 'Image output' });
// the first card should be the AWS card
await frame.locator('.pf-v6-c-card').first().click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame.getByRole('button', { name: 'Back', exact: true }).click();
frame.getByRole('heading', { name: 'Details' });
await frame.getByTestId('blueprint').fill(cockpitBlueprintname);
await expect(frame.getByTestId('blueprint')).toHaveValue(
cockpitBlueprintname,
);
await frame.getByRole('button', { name: 'Next', exact: true }).click();
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await frame.getByTestId('close-button-saveandbuild-modal').click();
await frame.getByRole('button', { name: 'Create blueprint' }).click();
await frame
.getByRole('textbox', { name: 'Search input' })
.fill(cockpitBlueprintname);
// the clickable blueprint cards are a bit awkward, so use the
// button's id instead
await frame.locator(`button[id="${cockpitBlueprintname}"]`).click();
await frame.getByTestId('blueprint-build-image-menu-option').click();
// make sure the image is present
await frame
.getByTestId('images-table')
.getByRole('button', { name: 'Details' })
.click();
frame.getByText('Build Information');
});
});

View file

@ -1,6 +0,0 @@
PLAYWRIGHT_USER="" # Required
PLAYWRIGHT_PASSWORD="" # Required
BASE_URL="https://stage.foo.redhat.com:1337" # Required
CI="" # This is set to true for CI jobs, if checking for CI do !!process.env.CI
TOKEN="" # This is handled programmatically.
PROXY="" # Set this if running directly against stage (not using "yarn local")

52
pr_check.sh Executable file
View file

@ -0,0 +1,52 @@
#!/bin/bash
# --------------------------------------------
# Export vars for helper scripts to use
# --------------------------------------------
# name of app-sre "application" folder this component lives in; needs to match for quay
export COMPONENT_NAME="image-builder-frontend"
# IMAGE should match the quay repo set by app.yaml in app-interface
export IMAGE="quay.io/cloudservices/image-builder-frontend"
export WORKSPACE=${WORKSPACE:-$APP_ROOT} # if running in jenkins, use the build's workspace
export APP_ROOT=$(pwd)
#16 is the default Node version. Change this to override it.
export NODE_BUILD_VERSION=16
COMMON_BUILDER=https://raw.githubusercontent.com/RedHatInsights/insights-frontend-builder-common/master
# --------------------------------------------
# Options that must be configured by app owner
# --------------------------------------------
export IQE_PLUGINS="image-builder"
export IQE_CJI_TIMEOUT="60m"
export IQE_MARKER_EXPRESSION="ui"
export IQE_SELENIUM="true"
export IQE_ENV="ephemeral"
export IQE_IMAGE_TAG="image-builder"
export RESERVE_DURATION="2h"
# bootstrap bonfire and it's config
CICD_URL=https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd
curl -s "$CICD_URL"/bootstrap.sh > .cicd_bootstrap.sh && source .cicd_bootstrap.sh
# # source is preferred to | bash -s in this case to avoid a subshell
source <(curl -sSL $COMMON_BUILDER/src/frontend-build.sh)
# reserve ephemeral namespace
export DEPLOY_FRONTENDS="true"
export EXTRA_DEPLOY_ARGS="provisioning sources content-sources rhsm-api-proxy --set-template-ref rhsm-api-proxy=master"
export APP_NAME="image-builder-crc"
export DEPLOY_TIMEOUT="1200"
export REF_ENV="insights-stage"
source "$CICD_ROOT"/deploy_ephemeral_env.sh
# Run smoke tests using a ClowdJobInvocation (preferred)
# The contents of this script can be found at:
# https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd/cji_smoke_test.sh
export COMPONENT_NAME="image-builder"
source "$CICD_ROOT"/cji_smoke_test.sh
# Post a comment with test run IDs to the PR
# The contents of this script can be found at:
# https://raw.githubusercontent.com/RedHatInsights/bonfire/master/cicd/post_test_results.sh
source "$CICD_ROOT"/post_test_results.sh

View file

@ -1,11 +0,0 @@
{
"extends": [
"github>konflux-ci/mintmaker//config/renovate/renovate.json"
],
"schedule": [
"on Monday after 3am and before 10am"
],
"ignorePaths": [
".pre-commit-config.yaml"
]
}

Binary file not shown.

View file

@ -1,56 +0,0 @@
#!/bin/bash
# Dumps details about the instance running the CI job.
PRIMARY_IP=$(ip route get 8.8.8.8 | head -n 1 | cut -d' ' -f7)
EXTERNAL_IP=$(curl --retry 5 -s -4 icanhazip.com)
PTR=$(curl --retry 5 -s -4 icanhazptr.com)
CPUS=$(nproc)
MEM=$(free -m | grep -oP '\d+' | head -n 1)
DISK=$(df --output=size -h / | sed '1d;s/[^0-9]//g')
HOSTNAME=$(uname -n)
USER=$(whoami)
ARCH=$(uname -m)
KERNEL=$(uname -r)
echo -e "\033[0;36m"
cat << EOF
------------------------------------------------------------------------------
CI MACHINE SPECS
------------------------------------------------------------------------------
Hostname: ${HOSTNAME}
User: ${USER}
Primary IP: ${PRIMARY_IP}
External IP: ${EXTERNAL_IP}
Reverse DNS: ${PTR}
CPUs: ${CPUS}
RAM: ${MEM} GB
DISK: ${DISK} GB
ARCH: ${ARCH}
KERNEL: ${KERNEL}
------------------------------------------------------------------------------
EOF
echo -e "\033[0m"
echo "List of system repositories:"
sudo yum repolist -v
echo "------------------------------------------------------------------------------"
echo "List of installed packages:"
rpm -qa | sort
echo "------------------------------------------------------------------------------"
# gcp runners don't use cloud-init and some of the images have python36 installed
if [[ "$RUNNER" != *"gcp"* ]];then
# Ensure cloud-init has completely finished on the instance. This ensures that
# the instance is fully ready to go.
while true; do
if [[ -f /var/lib/cloud/instance/boot-finished ]]; then
break
fi
echo -e "\n🤔 Waiting for cloud-init to finish running..."
sleep 5
done
fi

View file

@ -1,25 +0,0 @@
#!/bin/bash
set -euo pipefail
source /etc/os-release
sudo dnf install -y \
libappstream-glib
if [[ "$ID" == rhel && ${VERSION_ID%.*} == 10 ]]; then
sudo dnf install -y nodejs-npm \
sqlite # node fails to pull this in
elif [[ "$ID" == rhel ]]; then
sudo dnf install -y npm
elif [[ "$ID" == fedora ]]; then
sudo dnf install -y \
nodejs-npm \
sqlite \
gettext
fi
npm ci
make rpm
sudo dnf install -y rpmbuild/RPMS/noarch/*rpm

View file

@ -1,8 +0,0 @@
summary: run playwright tests
test: ./playwright_tests.sh
require:
- cockpit-image-builder
- podman
- nodejs
- nodejs-npm
duration: 30m

View file

@ -1,89 +0,0 @@
#!/bin/bash
set -euo pipefail
TMT_SOURCE_DIR=${TMT_SOURCE_DIR:-}
if [ -n "$TMT_SOURCE_DIR" ]; then
# Move to the directory with sources
cd "${TMT_SOURCE_DIR}/cockpit-image-builder"
npm ci
elif [ "${CI:-}" != "true" ]; then
# packit drops us into the schutzbot directory
cd ../
npm ci
fi
sudo systemctl enable --now cockpit.socket
sudo useradd admin -p "$(openssl passwd foobar)"
sudo usermod -aG wheel admin
echo "admin ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/admin-nopasswd"
function upload_artifacts {
if [ -n "${TMT_TEST_DATA:-}" ]; then
mv playwright-report "$TMT_TEST_DATA"/playwright-report
else
USER="$(whoami)"
sudo chown -R "$USER:$USER" playwright-report
mv playwright-report /tmp/artifacts/
fi
}
trap upload_artifacts EXIT
# to make package search work, the cdn repositories need to be replaced
# with the nightly repositories
sudo mkdir -p /etc/osbuild-composer/repositories
cat <<EOF | sudo tee -a /etc/osbuild-composer/repositories/rhel-9.json
{
"x86_64": [
{
"name": "baseos",
"baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9/compose/BaseOS/x86_64/os/",
"check_gpg": false
},
{
"name": "appstream",
"baseurl": "http://download.devel.redhat.com/rhel-9/nightly/RHEL-9/latest-RHEL-9/compose/AppStream/x86_64/os/",
"check_gpg": false
}
]
}
EOF
cat <<EOF | sudo tee -a /etc/osbuild-composer/repositories/rhel-10.json
{
"x86_64": [
{
"name": "baseos",
"baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10/latest-RHEL-10/compose/BaseOS/x86_64/os/",
"check_gpg": false
},
{
"name": "appstream",
"baseurl": "http://download.devel.redhat.com/rhel-10/nightly/RHEL-10/latest-RHEL-10/compose/AppStream/x86_64/os/",
"check_gpg": false
}
]
}
EOF
sudo systemctl enable --now osbuild-composer.socket osbuild-local-worker.socket
sudo systemctl start osbuild-worker@1
sudo podman run \
-e "PLAYWRIGHT_HTML_OPEN=never" \
-e "CI=true" \
-e "PLAYWRIGHT_USER=admin" \
-e "PLAYWRIGHT_PASSWORD=foobar" \
-e "CURRENTS_PROJECT_ID=${CURRENTS_PROJECT_ID:-}" \
-e "CURRENTS_RECORD_KEY=${CURRENTS_RECORD_KEY:-}" \
--net=host \
-v "$PWD:/tests" \
-v '/etc:/etc' \
-v '/etc/os-release:/etc/os-release' \
--privileged \
--rm \
--init \
mcr.microsoft.com/playwright:v1.51.1-noble \
/bin/sh -c "cd tests && npx -y playwright@1.51.1 test"

View file

@ -1,7 +0,0 @@
#!/bin/bash
# use tee, otherwise shellcheck complains
sudo journalctl --boot | tee journal-log >/dev/null
# copy journal to artifacts folder which is then uploaded to secure S3 location
cp journal-log "${ARTIFACTS:-/tmp/artifacts}"

30
schutzbot/sonarqube.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
set -euxo pipefail
SONAR_SCANNER_CLI_VERSION=${SONAR_SCANNER_CLI_VERSION:-4.6.2.2472}
export SONAR_SCANNER_OPTS="-Djavax.net.ssl.trustStore=schutzbot/RH-IT-Root-CA.keystore -Djavax.net.ssl.trustStorePassword=$KEYSTORE_PASS"
sudo dnf install -y unzip nodejs
curl "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_CLI_VERSION-linux.zip" -o sonar-scanner-cli.zip
unzip -q sonar-scanner-cli.zip
SONAR_SCANNER_CMD="sonar-scanner-$SONAR_SCANNER_CLI_VERSION-linux/bin/sonar-scanner"
SCANNER_OPTS="-Dsonar.projectKey=osbuild:image-builder-frontend -Dsonar.sources=. -Dsonar.host.url=https://sonarqube.corp.redhat.com -Dsonar.login=$SONAR_SCANNER_TOKEN"
# add options for branch analysis if not running on main
if [ "$CI_COMMIT_BRANCH" != "main" ];then
SCANNER_OPTS="$SCANNER_OPTS -Dsonar.pullrequest.branch=$CI_COMMIT_BRANCH -Dsonar.pullrequest.key=$CI_COMMIT_SHA -Dsonar.pullrequest.base=main"
fi
# run the sonar-scanner
eval "$SONAR_SCANNER_CMD $SCANNER_OPTS"
SONARQUBE_URL="https://sonarqube.corp.redhat.com/dashboard?id=osbuild%3Aimage-builder-frontend&pullRequest=$CI_COMMIT_SHA"
# Report back to GitHub
curl \
-u "${SCHUTZBOT_LOGIN}" \
-X POST \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/RedHatInsights/image-builder-frontend/statuses/${CI_COMMIT_SHA}" \
-d '{"state":"success", "description": "SonarQube scan sent for analysis", "context": "SonarQube", "target_url": "'"${SONARQUBE_URL}"'"}'

Some files were not shown because too many files have changed in this diff Show more