diff --git a/.ci-operator.yaml b/.ci-operator.yaml deleted file mode 100644 index 2cf3e07b..00000000 --- a/.ci-operator.yaml +++ /dev/null @@ -1,4 +0,0 @@ -build_root_image: - name: console-plugin-test-cypress - namespace: ci - tag: node22 \ No newline at end of file diff --git a/.claude/commands/e2e.md b/.claude/commands/e2e.md index 3b236fcf..3136e2b6 100644 --- a/.claude/commands/e2e.md +++ b/.claude/commands/e2e.md @@ -1,5 +1,5 @@ --- -allowed-tools: Read, Write, Edit, Bash(npx playwright test*), Bash(yarn test:e2e*), Bash(grep *), Bash(find *), Bash(npx tsc*), mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages, mcp__playwright__browser_click, mcp__playwright__browser_close +allowed-tools: Read, Write, Edit, Bash(npx playwright test*), Bash(make test-e2e*), Bash(grep *), Bash(find *), Bash(npx tsc*), mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages, mcp__playwright__browser_click, mcp__playwright__browser_close description: Scaffold and debug Playwright e2e tests for a feature --- @@ -100,7 +100,7 @@ Start by reading `docs/TESTING.md` (E2e Conventions section) for the full refere ```bash npx tsc --noEmit -p e2e/tsconfig.json - yarn test:e2e e2e/use-cases// + make test-e2e ARGS="e2e/use-cases//" ``` 6. **Debug failures** -- when a test fails: @@ -121,7 +121,7 @@ Start by reading `docs/TESTING.md` (E2e Conventions section) for the full refere 7. **Iterate** until all tests pass. Run the full suite once at the end: ```bash - yarn test:e2e + make test-e2e ``` ## Rules diff --git a/.devcontainer/Dockerfile.console b/.devcontainer/Dockerfile.console deleted file mode 100644 index 2d6f3c83..00000000 --- a/.devcontainer/Dockerfile.console +++ /dev/null @@ -1,20 +0,0 @@ -FROM quay.io/openshift/origin-console:latest -COPY --from=openshift/origin-cli:latest /usr/bin/oc /usr/local/bin/oc - -ENV OC_URL=$OC_URL -ENV OC_PASS=$OC_PASS -ENV OC_USER=$OC_USER -ENV OC_PLUGIN_NAME=$OC_PLUGIN_NAME - -USER root -CMD eval "oc login $OC_URL -u $OC_USER -p $OC_PASS --insecure-skip-tls-verify" && \ - /opt/bridge/bin/bridge -public-dir=/opt/bridge/static \ - -i18n-namespaces plugin__$OC_PLUGIN_NAME \ - -plugins $OC_PLUGIN_NAME=http://localhost:9001 \ - -k8s-mode-off-cluster-thanos=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.thanosPublicURL}') \ - -k8s-mode-off-cluster-endpoint=$(oc whoami --show-server) \ - -k8s-mode-off-cluster-skip-verify-tls=true \ - -k8s-auth-bearer-token=$(oc whoami --show-token) \ - -k8s-auth="bearer-token" \ - -user-auth="disabled" \ - -k8s-mode="off-cluster" diff --git a/.devcontainer/Dockerfile.plugin b/.devcontainer/Dockerfile.plugin deleted file mode 100644 index 59d4d0c8..00000000 --- a/.devcontainer/Dockerfile.plugin +++ /dev/null @@ -1,2 +0,0 @@ -FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:16 as build -COPY --from=openshift/origin-cli:latest /usr/bin/oc /usr/local/bin/oc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d438aa83..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "Console + Plugin", - "dockerComposeFile": "docker-compose.yml", - "service": "plugin", - "workspaceFolder": "/workspace", - - "initializeCommand": ".devcontainer/init.sh", - "postCreateCommand": "yarn && eval 'oc login $OC_URL -u $OC_USER -p $OC_PASS --insecure-skip-tls-verify'", - "forwardPorts": [9000, 9001], - "portsAttributes": { - "9000": { - "label": "Console" - }, - "9001": { - "label": "Plugin static files", - "onAutoForward": "silent" - } - }, - "features": {}, - "customizations": { - "vscode": { - "settings": {}, - "extensions": [ - "ms-azuretools.vscode-docker", - "ms-vscode.vscode-typescript-next", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ] - } - } -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index 57071ed2..00000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: '3.8' -# create dev.env with the following values: -# OC_URL -# OC_USER -# OC_PASS -# OC_PLUGIN_NAME -services: - console: - build: - context: .. - dockerfile: .devcontainer/Dockerfile.console - env_file: dev.env - restart: unless-stopped - healthcheck: - test: oc whoami - interval: 1m30s - timeout: 10s - retries: 5 - - plugin: - build: - context: .. - dockerfile: .devcontainer/Dockerfile.plugin - env_file: dev.env - depends_on: - - console - network_mode: service:console - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - # Cache local workspace and copy shell history. - volumes: - - ..:/workspace:cached - - ~/.bash_history:/root/.bash_history - - ~/.zsh_history:/root/.zsh_history diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh deleted file mode 100755 index 4ddee379..00000000 --- a/.devcontainer/init.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -if [[ ! -f .devcontainer/dev.env ]] -then - cat << EOF - env file 'dev.env' does not exist in .devcontainer, please create it and add the the correct values for your cluster. - OC_PLUGIN_NAME=my-plugin - OC_URL=https://api.example.com:6443 - OC_USER=kubeadmin - OC_PASS= -EOF - exit 2 -else - echo 'found 'dev.env' in .devcontainer' -fi - -# if one of the variables are missing, abort the build. -success=1 -! grep -q OC_PLUGIN_NAME= ".devcontainer/dev.env" && success=0 -! grep -q OC_URL= ".devcontainer/dev.env" && success=0 -! grep -q OC_USER= ".devcontainer/dev.env" && success=0 -! grep -q OC_PASS= ".devcontainer/dev.env" && success=0 - -if ((success)); then - echo 'dev.env is formatted correctly, proceeding.' -else - cat << EOF - dev.env is not formatted correctly, please add the the correct values for your cluster. - OC_PLUGIN_NAME=my-plugin - OC_URL=https://api.example.com:6443 - OC_USER=kubeadmin - OC_PASS= -EOF -exit 2 -fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index c125370c..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: CI - -permissions: - id-token: write # Required for signing - contents: read - packages: write - attestations: write - pages: write - -on: - push: - branches: - - master - pull_request: - branches: - - master - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - -env: - IMAGE: ghcr.io/${{ github.repository_owner }}/console-functions-plugin - -jobs: - # -------- - # CHECKS - # -------- - checks: - name: Lint and Test - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: "22" - cache: yarn - - - name: Enable Corepack - run: corepack enable - - - name: Install Dependencies - run: yarn install --immutable - - - name: Lint - run: yarn lint - - - name: Test - run: yarn test - - # ----------- - # BUILD IMAGE - # ----------- - build-image: - name: Build Image - needs: checks - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - - name: Build Image - uses: docker/build-push-action@v7 - env: - SOURCE_DATE_EPOCH: 0 - with: - context: . - push: false - provenance: false - # Multi-arch is required for OCP. - platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x - cache-from: type=gha - cache-to: type=gha,mode=max - - # ------------- - # PUBLISH IMAGE - # ------------- - publish: - name: Publish Image - needs: build-image - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - timeout-minutes: 30 - outputs: - digest: ${{ steps.build-and-push.outputs.digest }} - steps: - - uses: actions/checkout@v6 - - uses: docker/setup-qemu-action@v4 - - uses: docker/setup-buildx-action@v4 - - - name: Login to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push Image - id: build-and-push - uses: docker/build-push-action@v7 - env: - SOURCE_DATE_EPOCH: 0 - with: - context: . - push: true - provenance: false - platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x - tags: | - ${{ env.IMAGE }}:latest - ${{ env.IMAGE }}:sha-${{ github.sha }} - cache-from: type=gha - annotations: | - index:org.opencontainers.image.description=Serverless Functions Console Plugin for OpenShift - index:org.opencontainers.image.source=https://github.com/${{ github.repository }} - index:org.opencontainers.image.vendor=https://github.com/${{ github.repository }} - index:org.opencontainers.image.url=https://github.com/${{ github.repository }}/pkgs/container/console-functions-plugin - - # Attestation is required for OCP. - - name: Attest Build Provenance - uses: actions/attest-build-provenance@v4 - with: - subject-name: ${{ env.IMAGE }} - subject-digest: ${{ steps.build-and-push.outputs.digest }} - push-to-registry: true - - # ------------ - # DEPLOY PAGES - # ------------ - deploy-pages: - name: Deploy Pages - needs: publish - runs-on: ubuntu-latest - timeout-minutes: 5 - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v6 - - - name: Setup Helm - uses: azure/setup-helm@v5 - - - name: Generate deployment YAML and landing page - run: | - mkdir public - helm template console-functions-plugin charts/openshift-console-plugin \ - -n console-functions-plugin \ - --set "plugin.image=${{ env.IMAGE }}@${{ needs.publish.outputs.digest }}" \ - > public/plugin.yaml - cp pages/index.html public/index.html - - - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 - with: - path: ./public - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.husky/pre-commit b/.husky/pre-commit index 919f78de..28cb2923 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,15 +1,4 @@ #!/usr/bin/env bash set -euo pipefail -yarn i18n -yarn lint -yarn type-check - -cd backend -go vet ./... -unformatted="$(gofmt -l .)" -if [ -n "$unformatted" ]; then - echo "Go files not formatted. Run: gofmt -w backend/" - echo "$unformatted" - exit 1 -fi +make lint type-check verify diff --git a/AGENTS.md b/AGENTS.md index 79b716f8..94b72f7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,6 @@ No em dashes (`—`). Use commas, periods, or parentheses instead. | `docs/design/` | Design specs — "what to build" | | `docs/plans/active/` | Implementation plans in progress | | `docs/plans/completed/` | Finished plans | -| `.dev-env.json` | Dev server ports (backendPort, pluginPort, consolePort), written by init.sh | +| `.dev-env.json` | Dev server ports (backendPort, pluginPort, consolePort), written by hack/dev.sh | | `.dev-logs/` | Dev server log files (backend.log, webpack.log, console.log) | | `docs/references/ocp-dynamic-plugin-reference.md` | OCP dynamic plugin mechanics, i18n, extension points | diff --git a/Dockerfile b/Dockerfile index 4506b22d..e137b139 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,37 @@ -FROM --platform=$BUILDPLATFORM registry.access.redhat.com/ubi9/nodejs-22:latest AS build +FROM --platform=$BUILDPLATFORM registry.access.redhat.com/ubi9/nodejs-22:latest AS nodebuilder USER root ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 RUN npm i -g corepack && corepack enable WORKDIR /usr/src/app -COPY package.json yarn.lock .yarnrc.yml ./ +COPY Makefile package.json yarn.lock .yarnrc.yml ./ COPY .yarn/ .yarn/ -RUN yarn install --immutable +RUN make install-frontend COPY console-extensions.json tsconfig.json webpack.config.ts ./ COPY src/ src/ COPY locales/ locales/ COPY config/ config/ COPY testing/ testing/ -RUN yarn build +RUN make build-frontend -FROM --platform=$BUILDPLATFORM registry.access.redhat.com/ubi9/go-toolset:1.26 AS go-build -ARG TARGETOS -ARG TARGETARCH +FROM --platform=$BUILDPLATFORM registry.access.redhat.com/ubi9/go-toolset:1.26 AS gobuilder +ARG TARGETOS TARGETARCH +ENV GOOS=$TARGETOS GOARCH=$TARGETARCH +WORKDIR /opt/app-root/src -WORKDIR /opt/app-root/src/backend +COPY --chown=1001:0 Makefile ./ +COPY --chown=1001:0 backend/go.mod backend/go.sum backend/ +RUN make install-backend -COPY --chown=1001:0 backend/go.mod backend/go.sum /opt/app-root/src/backend/ -RUN go mod download - -COPY --chown=1001:0 --from=build /usr/src/app/dist /opt/app-root/src/backend/static -COPY --chown=1001:0 backend/ /opt/app-root/src/backend/ -RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-s -w" -o plugin-backend . +COPY --chown=1001:0 --from=nodebuilder /usr/src/app/dist backend/static +COPY --chown=1001:0 backend/ backend/ +RUN make build-backend FROM registry.access.redhat.com/ubi9-micro:latest - -COPY --from=go-build /etc/pki/tls/certs/ca-bundle.crt /etc/pki/tls/certs/ca-bundle.crt -COPY --from=go-build /opt/app-root/src/backend/plugin-backend /usr/bin/plugin-backend +COPY --from=gobuilder /opt/app-root/src/bin/plugin-backend /usr/bin/plugin-backend +COPY --from=gobuilder /etc/pki/tls/certs/ca-bundle.crt /etc/pki/tls/certs/ca-bundle.crt USER 1001 ENTRYPOINT ["plugin-backend"] diff --git a/Dockerfile.builder b/Dockerfile.builder new file mode 100644 index 00000000..423dda8d --- /dev/null +++ b/Dockerfile.builder @@ -0,0 +1,27 @@ +FROM registry.access.redhat.com/ubi9/go-toolset:1.26 + +USER root + +# Corepack + Yarn 4 (Node.js 22 is already in the base image) +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + COREPACK_HOME=/tmp/corepack +RUN npm i -g corepack && corepack enable + +# Helm (checksum-verified download instead of piping install script from main) +ARG HELM_VERSION=v3.21.3 +ARG HELM_SHA256_amd64=15e041a93a590dce8100f39385cd98c84a765c9e36aeeb9e2dc6ff9e4769e2e0 +ARG HELM_SHA256_arm64=67f58155079ff9ffab98ba5c88daff0ed9b542f3a4732f5dd426dde7dd0f5244 +RUN ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') \ + && eval "CHECKSUM=\${HELM_SHA256_${ARCH}}" \ + && curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-${ARCH}.tar.gz" -o /tmp/helm.tar.gz \ + && echo "${CHECKSUM} /tmp/helm.tar.gz" | sha256sum -c - \ + && tar xzf /tmp/helm.tar.gz --strip-components=1 -C /usr/local/bin "linux-${ARCH}/helm" \ + && rm /tmp/helm.tar.gz + +# Playwright system dependencies (Chromium) +RUN dnf install -y \ + nss atk at-spi2-atk cups-libs libdrm libXcomposite libXdamage libXrandr \ + mesa-libgbm pango alsa-lib libxkbcommon \ + && dnf clean all + +USER 1001 diff --git a/Makefile b/Makefile index fc079804..5408b2e0 100644 --- a/Makefile +++ b/Makefile @@ -1,43 +1,132 @@ PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) BACKEND_DIR := $(PROJECT_DIR)/backend + +# Backend build TEST_TIMEOUT ?= 120s +CGO_ENABLED ?= 0 +LDFLAGS ?= -s -w +BACKEND_BIN ?= $(PROJECT_DIR)/bin/plugin-backend GOLANGCI_LINT_VERSION ?= v2.12.2 GOLANGCI_LINT = go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) -.PHONY: help install lint build test build-backend test-backend lint-backend fmt-backend ci +# Image +CONTAINER_CMD ?= podman +IMAGE_TAG ?= localhost/faas-console-plugin:latest +PLATFORM ?= linux/amd64 + +# Cluster +PLUGIN_NAME ?= console-functions-plugin +NAMESPACE ?= console-functions-plugin +IMAGE ?= quay.io/redhat-user-workloads/ocp-serverless-tenant/faas-console-plugin:latest +KUBE_API_SERVER ?= https://api.example.com:6443 + +.PHONY: help install-frontend build-frontend lint-frontend unit-frontend type-check test-e2e verify \ + install-backend build-backend unit-backend lint-backend fmt-backend \ + image manifests deploy undeploy deploy-dev setup-serverless \ + dev dev-% \ + build lint unit e2e + .DEFAULT_GOAL := help -help: ## Display this help message - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) +help: ## Show this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} \ + /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } \ + /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } \ + ' $(MAKEFILE_LIST) -# ── Frontend ────────────────────────────────────────────────────────────────── +##@ Development -install: ## Install frontend dependencies - yarn install +dev: ## Start dev environment (also: dev-stop, dev-randomize-ports) + hack/dev.sh -lint: install ## Lint frontend source - yarn lint +dev-%: + hack/dev.sh --$* + +##@ Frontend -build: install ## Build frontend for production - NODE_ENV=production yarn webpack +install-frontend: ## Install frontend dependencies (yarn install --immutable) + yarn install --immutable -test: install ## Run frontend unit tests +build-frontend: install-frontend ## Build production bundle + yarn build + +lint-frontend: install-frontend ## Run eslint and stylelint + yarn lint + +unit-frontend: install-frontend ## Run Vitest unit tests yarn test -# ── Backend ─────────────────────────────────────────────────────────────────── +type-check: ## Run TypeScript compiler check + yarn type-check + +test-e2e: install-frontend ## Run Playwright e2e tests (ARGS="--headed") + yarn test:e2e $(ARGS) + +verify: install-frontend ## Verify i18n freshness and yarn deduplication + yarn i18n + @GIT_STATUS="$$(git status --short --untracked-files -- locales)"; \ + if [ -n "$$GIT_STATUS" ]; then \ + echo "i18n files are not up to date. Run 'yarn i18n' then commit changes."; \ + git --no-pager diff; \ + exit 1; \ + fi + @if ! yarn dedupe --strategy highest --check; then \ + echo "Duplicate version resolutions found. Run 'yarn dedupe' and commit the updated yarn.lock."; \ + exit 1; \ + fi + +##@ Backend -build-backend: ## Compile the Go backend - cd $(BACKEND_DIR) && go build ./... +install-backend: ## Download Go module dependencies + go -C $(BACKEND_DIR) mod download -test-backend: ## Run backend unit and integration tests - cd $(BACKEND_DIR) && go test ./... -timeout $(TEST_TIMEOUT) +build-backend: ## Compile Go binary to bin/ + @mkdir -p $(dir $(BACKEND_BIN)) + CGO_ENABLED=$(CGO_ENABLED) \ + $(if $(GOOS),GOOS=$(GOOS)) \ + $(if $(GOARCH),GOARCH=$(GOARCH)) \ + go -C $(BACKEND_DIR) build -ldflags="$(LDFLAGS)" -o $(BACKEND_BIN) . -lint-backend: ## Lint backend source with golangci-lint +unit-backend: ## Run Go unit tests + go -C $(BACKEND_DIR) test ./... -timeout $(TEST_TIMEOUT) + +lint-backend: ## Run golangci-lint cd $(BACKEND_DIR) && $(GOLANGCI_LINT) run ./... -fmt-backend: ## Format backend source (gofmt + goimports via golangci-lint --fix) +fmt-backend: ## Auto-fix lint issues (golangci-lint --fix) cd $(BACKEND_DIR) && $(GOLANGCI_LINT) run --fix ./... -# ── CI ──────────────────────────────────────────────────────────────────────── +##@ Image + +image: ## Build container image + $(CONTAINER_CMD) build --platform $(PLATFORM) -t $(IMAGE_TAG) . + +##@ Cluster + +manifests: ## Render Helm chart to backend/static/plugin.yaml + helm template $(PLUGIN_NAME) charts/openshift-console-plugin \ + -n $(NAMESPACE) \ + --set "plugin.image=$(IMAGE)" \ + --set "plugin.apiServerURL=$(KUBE_API_SERVER)" \ + > $(BACKEND_DIR)/static/plugin.yaml + +deploy: ## Deploy plugin to cluster (defaults to quay.io latest image) + hack/deploy.sh + +undeploy: ## Remove plugin from cluster + helm uninstall $(PLUGIN_NAME) -n $(NAMESPACE) + +deploy-dev: ## Build and deploy to cluster (dev) + hack/deploy-dev.sh + +setup-serverless: ## Install Serverless operator and Knative Serving + hack/setup-serverless.sh + +##@ Aggregate + +lint: lint-frontend lint-backend ## Lint frontend and backend + +unit: unit-frontend unit-backend ## Run all unit tests -ci: lint build test build-backend test-backend lint-backend ## Run all checks +e2e: ## Run Prow e2e flow (CI only) + hack/test-prow-e2e.sh diff --git a/README.md b/README.md index 60e42c00..2d6e13d5 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ A Functions-as-a-Service PoC UI for the OpenShift Web Console. Developers create, edit, and deploy serverless functions without CLI knowledge. -Built as an [OpenShift Console dynamic plugin](https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk) using React, TypeScript, and PatternFly 6. - -Check out the **[Github page](https://github.com/openshift/faas-console-plugin)** for a quick start or read ahead. +Built as an [OpenShift Console dynamic plugin](https://github.com/openshift/console/tree/main/frontend/packages/console-dynamic-plugin-sdk) using Go, React, TypeScript, and PatternFly 6. ## Team Values @@ -46,43 +44,40 @@ Check out the **[Github page](https://github.com/openshift/faas-console-plugin)* ### Prerequisites - [oc](https://console.redhat.com/openshift/downloads) CLI -- An [OpenShift 4.19 cluster](https://console.redhat.com/openshift/create) +- [Helm](https://helm.sh/docs/intro/install/) +- An [OpenShift 4.22+ cluster](https://console.redhat.com/openshift/create) - Github [*Personal Access Token*](https://github.com/settings/personal-access-tokens) with *administration*, *content*, *secret* and *workflow* write permissions in all repositories -### Quick install +### Install ```shell -oc new-project console-functions-plugin -oc apply -f https://functions-dev.github.io/ocp-console-plugin/plugin.yaml +make deploy ``` -### Manual install (requires [Helm](https://helm.sh)) +To deploy a specific build, use its git commit SHA as the tag: ```shell -oc new-project console-functions-plugin -helm upgrade -i console-functions-plugin charts/openshift-console-plugin \ - -n console-functions-plugin --create-namespace \ - --set "plugin.image=ghcr.io/functions-dev/ocp-console-plugin-functions-plugin:latest@sha256:" +make deploy IMAGE=quay.io/redhat-user-workloads/ocp-serverless-tenant/faas-console-plugin: ``` -To deploy a specific build, use its git commit SHA as the tag: +Available image tags are listed in the [container registry](https://quay.io/repository/redhat-user-workloads/ocp-serverless-tenant/faas-console-plugin?tab=tags). + +To remove the plugin: ```shell ---set "plugin.image=ghcr.io/functions-dev/ocp-console-plugin-functions-plugin:sha-" +make undeploy ``` -Available image tags are listed in the [container registry](https://github.com/functions-dev/ocp-console-plugin/pkgs/container/console-functions-plugin). Consult the chart [values](charts/openshift-console-plugin/values.yaml) file for additional parameters. - ## Development ### Prerequisites -- [Node.js](https://nodejs.org/en/) (v18+) +- [Node.js](https://nodejs.org/en/) (v22+) - [Yarn](https://yarnpkg.com) (v4) -- [Go](https://go.dev/dl/) (v1.24+) +- [Go](https://go.dev/dl/) (v1.26+) - [Helm](https://helm.sh/docs/intro/install/) - [oc](https://console.redhat.com/openshift/downloads) CLI -- [Docker](https://www.docker.com) or [podman 3.2.0+](https://podman.io) +- [Podman](https://podman.io) (v3.2.0+) - An [OpenShift cluster](https://console.redhat.com/openshift/create) - Github [*Personal Access Token*](https://github.com/settings/personal-access-tokens) with *administration*, *content* and *workflow* write permissions in all repositories - [gh](https://cli.github.com/) CLI (optional, enables your agent to create/update PRs) @@ -90,69 +85,41 @@ Available image tags are listed in the [container registry](https://github.com/f - [Superpowers](https://github.com/obra/superpowers) (optional, enables your coding agents to brainstorm, write plans, use tdd, etc.) - [Jira CLI](https://github.com/ankitpokhrel/jira-cli/wiki/Installation) (optional, enables your coding agent to read Jira tickets) -### Setup - -1. `oc login` to your OpenShift cluster -2. `yarn install` -3. `./init.sh` - -This builds the pages assets (plugin.yaml, landing page), compiles the Go backend, starts the webpack dev server, and launches the OpenShift console in a container. Navigate to to see the running plugin. - -To stop the dev environment: +### Cluster setup ```shell -./init.sh --stop +oc login ... +make setup-serverless # install Serverless operator + Knative Serving (optional) ``` -To use random ports (useful when defaults are already in use): +### Setup ```shell -./init.sh --randomize-ports +make dev # build + start webpack + console container +make dev-stop # stop dev environment +make dev-randomize-ports # start with random ports (when defaults are in use) ``` -### Running e2e tests +Navigate to to see the running plugin. -E2e tests use Playwright against a running cluster. Set `BRIDGE_GITHUB_PAT` in `.env` with a GitHub PAT that has `repo` scope, then: +### Testing ```shell -yarn test:e2e # all tests, headless -yarn test:e2e:report # open HTML report -yarn test:e2e:headed # visible browser -yarn test:e2e:ui # interactive UI mode +make unit # frontend + backend unit tests +make test-e2e # Playwright e2e (requires make dev running) +make test-e2e ARGS="--headed" # visible browser +make test-e2e ARGS="--ui" # interactive UI mode ``` See [docs/TESTING.md](docs/TESTING.md#e2e-conventions) for full conventions, helpers, and environment variables. -### Viewing GitHub Pages locally +### Deploy to cluster -The landing page served at [functions-dev.github.io/ocp-console-plugin](https://functions-dev.github.io/ocp-console-plugin/) is built from `pages/index.html` and the Helm chart. The `init.sh` script generates these assets into `backend/static/` automatically, so the running backend serves them at . +To deploy a production-like image to the cluster instead of running locally: -## Docker image - -Before you can deploy your plugin on a cluster, you must build an image and -push it to an image registry. - -1. Build the image: - - ```sh - docker build -t quay.io/my-repository/my-plugin:latest . - ``` - -2. Run the image: - - ```sh - docker run -it --rm -d -p 9001:80 quay.io/my-repository/my-plugin:latest - ``` - -3. Push the image: - - ```sh - docker push quay.io/my-repository/my-plugin:latest - ``` - -NOTE: If you have a Mac with Apple silicon, you will need to add the flag -`--platform=linux/amd64` when building the image to target the correct platform -to run in-cluster. +```shell +make deploy-dev # build image, push to internal registry, deploy +``` ## i18n @@ -187,14 +154,14 @@ namespace. For example: } ``` -Running `yarn i18n` extracts translatable strings into the JSON files in the -`locales` folder and regenerates TypeScript types in `config/i18next/`. The -extraction configuration is in `i18next.config.ts`. +Running `make verify` checks i18n freshness and extracts translatable strings +into the JSON files in the `locales` folder. The extraction configuration is in +`i18next.config.ts`. ## Linting -This project adds prettier, eslint, and stylelint. Linting can be run with -`yarn run lint`. +This project adds prettier, eslint, stylelint, and golangci-lint. Linting can +be run with `make lint`. The stylelint config disallows defining colors since these cause problems with dark mode. Use [PatternFly semantic tokens](https://www.patternfly.org/tokens/all-patternfly-tokens) diff --git a/backend/cluster/kubeconfig_test.go b/backend/cluster/kubeconfig_test.go index 5e5c76fd..5999379b 100644 --- a/backend/cluster/kubeconfig_test.go +++ b/backend/cluster/kubeconfig_test.go @@ -42,9 +42,7 @@ func fullFakeClient(token string) (*k8sClient, *fake.Clientset) { var _ = Describe("GenerateKubeconfig", func() { It("returns a valid kubeconfig with the token and server URL", func() { - cs := fake.NewSimpleClientset() - cs.PrependReactor("create", "serviceaccounts", tokenReactor("sa-token-value")) - cl := &k8sClient{clientset: cs} + cl, _ := fullFakeClient("sa-token-value") kubeconfig, err := GenerateKubeconfig(context.Background(), cl, "default", fakeAPIURL, nil) @@ -63,9 +61,7 @@ var _ = Describe("GenerateKubeconfig", func() { }) It("embeds the CA certificate when the cluster uses a private CA", func() { - cs := fake.NewSimpleClientset() - cs.PrependReactor("create", "serviceaccounts", tokenReactor("sa-token-value")) - cl := &k8sClient{clientset: cs} + cl, _ := fullFakeClient("sa-token-value") caCert := []byte("-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----\n") kubeconfig, err := GenerateKubeconfig(context.Background(), cl, "default", fakeAPIURL, caCert) @@ -95,9 +91,7 @@ var _ = Describe("GenerateKubeconfig", func() { }) It("returns an error when the external API server URL is empty", func() { - cs := fake.NewSimpleClientset() - cs.PrependReactor("create", "serviceaccounts", tokenReactor("sa-token-value")) - cl := &k8sClient{clientset: cs} + cl, _ := fullFakeClient("sa-token-value") _, err := GenerateKubeconfig(context.Background(), cl, "default", "", nil) diff --git a/backend/handler/handler_test.go b/backend/handler/handler_test.go index 7f2bd9cd..ff25f1fc 100644 --- a/backend/handler/handler_test.go +++ b/backend/handler/handler_test.go @@ -64,7 +64,6 @@ func (s *scmStub) DeleteRepo(ctx context.Context, owner, repo string) error { return nil } - func withSCMStub(stub scm.Client) { orig := config.SCMRegistry config.SCMRegistry = scm.Registry{ diff --git a/backend/scm/github/client_test.go b/backend/scm/github/client_test.go index fdd0edfa..d47fc35e 100644 --- a/backend/scm/github/client_test.go +++ b/backend/scm/github/client_test.go @@ -22,7 +22,6 @@ func newClient(handler http.HandlerFunc) scm.Client { return NewWithBaseURL("test-pat", srv.URL) } - var _ = Describe("GitHub SCM client", func() { Describe("GetUser", func() { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 93022d89..8bcbb4e2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -119,7 +119,7 @@ Go + `net/http` standard library. Key dependencies: ### Key Decisions **Cluster host resolution: explicit parameter via `--kube-host` flag** -`cluster.New(host, token, caCert)` accepts the API server URL as an explicit parameter. Empty host triggers `rest.InClusterConfig()` (production pods). In dev, `init.sh` passes `--kube-host $KUBE_API_SERVER` to the backend binary; in tests, it is passed directly to `cluster.New`. Env var injection (`KUBERNETES_SERVICE_HOST`) was explicitly rejected as it abuses a Kubernetes-standardized variable and creates hidden ambient state. +`cluster.New(host, token, caCert)` accepts the API server URL as an explicit parameter. Empty host triggers `rest.InClusterConfig()` (production pods). In dev, `hack/dev.sh` passes `--kube-host $KUBE_API_SERVER` to the backend binary; in tests, it is passed directly to `cluster.New`. Env var injection (`KUBERNETES_SERVICE_HOST`) was explicitly rejected as it abuses a Kubernetes-standardized variable and creates hidden ambient state. **External API URL resolved at Helm install time** The URL embedded in generated kubeconfigs (`externalAPIServerURL`) comes from the Infrastructure CR (`config.openshift.io/v1/Infrastructure/cluster`) via Helm `lookup` at install time, injected as `--external-api-server-url`. It is not fetched at runtime. This eliminates the need for a `ClusterRole` to query the Infrastructure CR from within the pod. diff --git a/docs/TESTING.md b/docs/TESTING.md index d74b9935..14cf3503 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -143,13 +143,19 @@ afterEach(() => { }); ``` +### Running unit tests + +```bash +make unit-frontend +``` + ## E2e Conventions E2e tests run against a real OpenShift cluster. GitHub API calls are intercepted with `page.route()` mocks, while K8s API calls go to the real cluster. Each test file covers a single use case, exercising a flow from start to finish with `test.step` for structure. ### Prerequisites -- A running OpenShift cluster with the plugin deployed (or a local dev environment via `init.sh`) +- A running OpenShift cluster with the plugin deployed (or a local dev environment via `make dev`) - The OpenShift Serverless operator should be installed on the cluster (tests install it automatically, but first install takes several minutes) ### Environment @@ -164,12 +170,12 @@ E2e tests run against a real OpenShift cluster. GitHub API calls are intercepted ### Running ```bash -yarn test:e2e # all tests, headless -yarn test:e2e e2e/use-cases/creation/ # one use-case directory -yarn test:e2e e2e/use-cases/delete/function-delete.test.ts # single file -yarn test:e2e:headed # visible browser -yarn test:e2e:ui # interactive UI mode -yarn test:e2e:report # open HTML report +make test-e2e # all tests, headless +make test-e2e ARGS="e2e/use-cases/creation/" # one use-case directory +make test-e2e ARGS="e2e/use-cases/delete/function-delete.test.ts" # single file +make test-e2e ARGS="--headed" # visible browser +make test-e2e ARGS="--ui" # interactive UI mode +yarn test:e2e:report # open HTML report (no make target) ``` ### File Structure @@ -282,15 +288,15 @@ E2e tests also run in CI via Prow/ci-operator against an ephemeral OCP cluster o 1. ci-operator provisions an ephemeral cluster from the `openshift-org-aws` pool 2. The `install-operators` pre-step installs the Serverless operator from `redhat-operators` 3. ci-operator builds the plugin container image from the Dockerfile -4. `test-prow-e2e.sh` deploys the plugin to the cluster via Helm, enables it on the console, then runs Playwright headless +4. `hack/test-prow-e2e.sh` deploys the plugin to the cluster via Helm, enables it on the console, then runs Playwright headless 5. Artifacts (JUnit XML, HTML report, screenshots, traces) are copied to `$ARTIFACT_DIR` for Prow Spyglass ### Configuration | File | Purpose | |------|---------| -| `.ci-operator.yaml` | Build root image config (Node 22 base image for the `src` container) | -| `test-prow-e2e.sh` | Prow e2e test entrypoint (reads cluster credentials, deploys plugin, runs tests) | +| `Dockerfile.builder` | Builder image (Go 1.26 + Node 22 + Yarn 4 + Helm for the `src` container) | +| `hack/test-prow-e2e.sh` | Prow e2e test entrypoint (reads cluster credentials, deploys plugin, runs tests) | The ci-operator job config lives in the `openshift/release` repo at `ci-operator/config/openshift/faas-console-plugin/`. @@ -298,9 +304,10 @@ The ci-operator job config lives in the `openshift/release` repo at `ci-operator | Job | Type | What it runs | |-----|------|-------------| -| `lint` | Container test (no cluster) | `yarn install --immutable && yarn run lint && yarn run build` | -| `unit` | Container test (no cluster) | `yarn install --immutable && yarn run test` | -| `e2e-aws` | Cluster test | `test-prow-e2e.sh` against an ephemeral OCP cluster | +| `images` | Image build | Builds the plugin container image from `Dockerfile` (automatic ci-operator job) | +| `lint` | Container test (no cluster) | `make lint` | +| `unit` | Container test (no cluster) | `make unit` | +| `e2e-aws` | Cluster test | `make e2e` against an ephemeral OCP cluster (with Serverless + Pipelines operators pre-installed) | ### Local vs CI differences @@ -388,5 +395,5 @@ Use `DescribeTable` / `Entry` for validation and error variants to keep them con ### Running ```bash -make test-backend +make unit-backend ``` diff --git a/e2e/helpers/cluster.ts b/e2e/helpers/cluster.ts index f880f672..45ec9243 100644 --- a/e2e/helpers/cluster.ts +++ b/e2e/helpers/cluster.ts @@ -90,51 +90,51 @@ async function ensureServerlessOperator(page: Page): Promise { const csvPath = `${K8S}/apis/operators.coreos.com/v1alpha1/namespaces/${SUBSCRIPTION_NS}/clusterserviceversions`; const csvCheck = await page.request.get(csvPath, { headers }); - if (csvCheck.ok()) { - const body = await csvCheck.json(); - if (body.items?.some((csv: CsvItem) => isServerlessReady(csv))) return; - } - - await ensureNamespace(page, SUBSCRIPTION_NS); - - await createResourceIfNotExists( - page, - `${K8S}/apis/operators.coreos.com/v1/namespaces/${SUBSCRIPTION_NS}/operatorgroups`, - { - apiVersion: 'operators.coreos.com/v1', - kind: 'OperatorGroup', - metadata: { name: 'serverless-operators', namespace: SUBSCRIPTION_NS }, - }, - ); - - await createResourceIfNotExists( - page, - `${K8S}/apis/operators.coreos.com/v1alpha1/namespaces/${SUBSCRIPTION_NS}/subscriptions`, - { - apiVersion: 'operators.coreos.com/v1alpha1', - kind: 'Subscription', - metadata: { name: 'serverless-operator', namespace: SUBSCRIPTION_NS }, - spec: { - channel: 'stable', - name: 'serverless-operator', - source: 'redhat-operators', - sourceNamespace: 'openshift-marketplace', - installPlanApproval: 'Automatic', + const csvReady = + csvCheck.ok() && (await csvCheck.json()).items?.some((csv: CsvItem) => isServerlessReady(csv)); + + if (!csvReady) { + await ensureNamespace(page, SUBSCRIPTION_NS); + + await createResourceIfNotExists( + page, + `${K8S}/apis/operators.coreos.com/v1/namespaces/${SUBSCRIPTION_NS}/operatorgroups`, + { + apiVersion: 'operators.coreos.com/v1', + kind: 'OperatorGroup', + metadata: { name: 'serverless-operators', namespace: SUBSCRIPTION_NS }, }, - }, - ); - - await expect - .poll( - async () => { - const res = await page.request.get(csvPath, { headers }); - if (!res.ok()) return false; - const body = await res.json(); - return body.items?.some((csv: CsvItem) => isServerlessReady(csv)) ?? false; + ); + + await createResourceIfNotExists( + page, + `${K8S}/apis/operators.coreos.com/v1alpha1/namespaces/${SUBSCRIPTION_NS}/subscriptions`, + { + apiVersion: 'operators.coreos.com/v1alpha1', + kind: 'Subscription', + metadata: { name: 'serverless-operator', namespace: SUBSCRIPTION_NS }, + spec: { + channel: 'stable', + name: 'serverless-operator', + source: 'redhat-operators', + sourceNamespace: 'openshift-marketplace', + installPlanApproval: 'Automatic', + }, }, - { timeout: 240_000, intervals: [2_000] }, - ) - .toBe(true); + ); + + await expect + .poll( + async () => { + const res = await page.request.get(csvPath, { headers }); + if (!res.ok()) return false; + const body = await res.json(); + return body.items?.some((csv: CsvItem) => isServerlessReady(csv)) ?? false; + }, + { timeout: 240_000, intervals: [2_000] }, + ) + .toBe(true); + } await ensureNamespace(page, SERVING_NS); diff --git a/hack/builder-run.sh b/hack/builder-run.sh new file mode 100755 index 00000000..77a7f4f8 --- /dev/null +++ b/hack/builder-run.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Run a command inside the builder container. +# +# Usage: +# ./hack/builder-run.sh make lint +# ./hack/builder-run.sh make unit +# +# E2E (requires KUBECONFIG, PLUGIN_PULL_SPEC, and BRIDGE_KUBEADMIN_PASSWORD). +# Tests that deploy Knative services need the Serverless Operator installed +# on the cluster beforehand. Run "make setup-serverless" once before the +# first e2e run (it is idempotent). +# +# KUBECONFIG= \ +# PLUGIN_PULL_SPEC= \ +# BRIDGE_KUBEADMIN_PASSWORD= \ +# ./hack/builder-run.sh make e2e +# + +source "$(dirname "${BASH_SOURCE[0]}")/lib/log.sh" + +BUILDER_IMAGE="${BUILDER_IMAGE:-localhost/faas-console-plugin-builder:latest}" +CONTAINER_CMD=podman + +log::step "Building builder image" +if ! $CONTAINER_CMD build -f Dockerfile.builder -t "$BUILDER_IMAGE" . >/dev/null; then + log::error "Failed to build builder image" + exit 1 +fi +log::info "Done" + +ENTRYPOINT=$(cat <<'SCRIPT' +# In presubmits, oc is injected by ci-operator (cli: latest). Locally we download it. +OC_ARCH=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/amd64/') +curl -fsSL "https://mirror.openshift.com/pub/openshift-v4/${OC_ARCH}/clients/ocp/stable/openshift-client-linux.tar.gz" | tar xz -C /tmp oc +export PATH=/tmp:$PATH + +if [ -n "${BRIDGE_KUBEADMIN_PASSWORD:-}" ]; then + echo -n "$BRIDGE_KUBEADMIN_PASSWORD" > /tmp/kubeadmin-password + export KUBEADMIN_PASSWORD_FILE=/tmp/kubeadmin-password +fi + + +# Source is mounted read-only at /src to prevent container writes leaking to the host. +# Copy to a writable directory so build tools (yarn, go) can write output. +cp -r /src/. . + +exec "$@" +SCRIPT +) + +ENV_STR=() +VOLUME_MOUNT=() +ARTIFACT_DIR="${ARTIFACT_DIR:-$(pwd)/.e2e/artifacts}" +mkdir -p "$ARTIFACT_DIR" +VOLUME_MOUNT+=("-v" "$ARTIFACT_DIR:/tmp/artifacts:Z") +ENV_STR+=("-e" "ARTIFACT_DIR=/tmp/artifacts") + +if [ -n "${KUBECONFIG:-}" ]; then + VOLUME_MOUNT+=("-v" "$KUBECONFIG:/kube/config:ro,Z") + ENV_STR+=("-e" "KUBECONFIG=/kube/config") +fi + +for VAR in PLUGIN_PULL_SPEC BRIDGE_KUBEADMIN_PASSWORD; do + if [ -n "${!VAR:-}" ]; then + ENV_STR+=("-e" "$VAR") + fi +done + +NETWORK_OPTS=() +if grep -q 'api.crc.testing' "${KUBECONFIG:-/dev/null}" 2>/dev/null; then + if [[ "$(uname)" == "Linux" ]]; then + NETWORK_OPTS+=(--net=host) + else + NETWORK_OPTS+=(--add-host "api.crc.testing:host-gateway") + NETWORK_OPTS+=(--add-host "console-openshift-console.apps-crc.testing:host-gateway") + NETWORK_OPTS+=(--add-host "oauth-openshift.apps-crc.testing:host-gateway") + fi +fi + +log::step "Running: $*" +if $CONTAINER_CMD run "${ENV_STR[@]}" --rm -it ${NETWORK_OPTS[@]+"${NETWORK_OPTS[@]}"} \ + --shm-size=512m \ + "${VOLUME_MOUNT[@]}" \ + -v "$(pwd)":/src:ro,Z \ + -w /opt/app-root/src \ + "$BUILDER_IMAGE" sh -c "$ENTRYPOINT" -- "$@"; then + log::info "SUCCESS" +else + log::error "FAILED" + exit 1 +fi diff --git a/hack/deploy-dev.sh b/hack/deploy-dev.sh new file mode 100755 index 00000000..9e25d18f --- /dev/null +++ b/hack/deploy-dev.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Full dev deployment workflow: build the container image locally, push it to +# the cluster's internal image registry, and deploy the plugin via Helm. +# +# For deploying a pre-built image (e.g. from CI), use make deploy (deploy.sh) instead. +# +# Prerequisites: oc login +# Optional: make setup-serverless (for full Knative/Serverless functionality) +# Usage: make deploy-dev [IMAGE_TAG=...] [NAMESPACE=...] + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/log.sh" + +NAMESPACE="${NAMESPACE:-console-functions-plugin}" +IMAGE_TAG="${IMAGE_TAG:-localhost/faas-console-plugin:latest}" +PLUGIN_NAME="console-functions-plugin" +CONTAINER_CMD=podman +REGISTRY_PORT=5001 +INTERNAL_REGISTRY="image-registry.openshift-image-registry.svc:5000" + +# --- Prerequisites --- + +if ! command -v oc &>/dev/null; then + log::error "oc CLI not found. Install from https://console.redhat.com/openshift/downloads" + exit 1 +fi + +if ! oc whoami &>/dev/null; then + log::error "Not logged in to OpenShift. Run 'oc login' first." + exit 1 +fi + +# --- Build --- + +log::step "Building image" +make image IMAGE_TAG="$IMAGE_TAG" + +# --- Push to internal registry --- + +log::step "Pushing to internal registry" + +# Port-forward the internal registry service. +# On macOS, podman runs in a VM that can't reach localhost, so we bind to all +# interfaces and push via the host's LAN IP. On Linux, localhost works directly. +if [[ "$(uname)" == "Darwin" ]]; then + PUSH_TARGET=$(ifconfig | awk '/inet / && !/127.0.0.1/ {print $2; exit}') + if [ -z "$PUSH_TARGET" ]; then + log::error "Could not determine host IP address." + exit 1 + fi + PUSH_TARGET="${PUSH_TARGET}:${REGISTRY_PORT}" +else + PUSH_TARGET="localhost:${REGISTRY_PORT}" +fi + +oc get namespace "$NAMESPACE" &>/dev/null 2>&1 || oc create namespace "$NAMESPACE" + +log::info "Port-forwarding registry to ${PUSH_TARGET}..." +kubectl port-forward svc/image-registry \ + --address='::' --address='0.0.0.0' \ + "${REGISTRY_PORT}:5000" \ + -n openshift-image-registry & +PF_PID=$! +trap "kill $PF_PID 2>/dev/null || true" EXIT +sleep 5 + +PUSH_IMAGE="${PUSH_TARGET}/${NAMESPACE}/${PLUGIN_NAME}:latest" + +log::info "Logging in to registry..." +$CONTAINER_CMD login "$PUSH_TARGET" \ + -u unused -p "$(oc whoami -t)" --tls-verify=false + +$CONTAINER_CMD tag "$IMAGE_TAG" "$PUSH_IMAGE" + +DIGEST_FILE=$(mktemp /tmp/plugin-digest.XXXXXX) +log::info "Pushing image..." +$CONTAINER_CMD push "$PUSH_IMAGE" --tls-verify=false --digestfile "$DIGEST_FILE" + +DIGEST=$(cat "$DIGEST_FILE") +rm -f "$DIGEST_FILE" +DEPLOY_IMAGE="${INTERNAL_REGISTRY}/${NAMESPACE}/${PLUGIN_NAME}:latest@${DIGEST}" + +kill $PF_PID 2>/dev/null || true +trap - EXIT + +# --- Deploy --- + +log::step "Deploying plugin via Helm" +make deploy IMAGE="$DEPLOY_IMAGE" NAMESPACE="$NAMESPACE" diff --git a/hack/deploy.sh b/hack/deploy.sh new file mode 100755 index 00000000..4c31c1bf --- /dev/null +++ b/hack/deploy.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy the plugin to an OpenShift cluster using the Helm chart. +# Installs (or upgrades) the chart, waits for rollout, enables the plugin +# on the console, and restarts the console pods. +# +# For the full dev workflow (build image + push to internal registry + deploy), +# see deploy-dev.sh instead. +# +# Usage: make deploy [IMAGE=...] [NAMESPACE=...] +# Prerequisites: oc login, Helm + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/log.sh" + +PLUGIN_NAME="${PLUGIN_NAME:-console-functions-plugin}" +NAMESPACE="${NAMESPACE:-console-functions-plugin}" +IMAGE="${IMAGE:-quay.io/redhat-user-workloads/ocp-serverless-tenant/faas-console-plugin:latest}" + +log::step "Deploying plugin" + +log::info "Installing Helm chart..." +helm upgrade -i "$PLUGIN_NAME" charts/openshift-console-plugin \ + -n "$NAMESPACE" --create-namespace \ + --set "plugin.image=$IMAGE" + +log::info "Waiting for rollout..." +oc rollout status deployment/"$PLUGIN_NAME" -n "$NAMESPACE" --timeout=300s + +log::info "Waiting for ConsolePlugin CR..." +for i in $(seq 1 60); do + if oc get consoleplugins "$PLUGIN_NAME" &>/dev/null; then + log::info "ConsolePlugin CR found." + break + fi + if [ "$i" -eq 60 ]; then + log::error "ConsolePlugin CR did not appear within 120s." + oc get all -n "$NAMESPACE" + exit 1 + fi + sleep 2 +done + +ENABLED=$(oc get consoles.operator.openshift.io cluster -o jsonpath='{.spec.plugins}' 2>/dev/null || true) +if echo "$ENABLED" | grep -q "$PLUGIN_NAME"; then + log::info "Plugin $PLUGIN_NAME already enabled on console." +else + log::info "Patching console operator to enable plugin..." + oc patch consoles.operator.openshift.io cluster \ + --type=json \ + --patch='[{"op":"add","path":"/spec/plugins/-","value":"'"${PLUGIN_NAME}"'"}]' +fi + +log::info "Restarting console pods to pick up plugin changes..." +oc rollout restart deployment/console -n openshift-console +oc rollout status deployment/console -n openshift-console --timeout=300s + +CONSOLE_URL=$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}') + +log::step "Plugin deployed successfully" +log::link "API" "$CONSOLE_URL/api/proxy/plugin/$PLUGIN_NAME/backend/" +log::link "Console" "$CONSOLE_URL/faas" +log::hint "For full Knative integration: make setup-serverless" diff --git a/init.sh b/hack/dev.sh similarity index 71% rename from init.sh rename to hack/dev.sh index 7548452d..62a8d7e2 100755 --- a/init.sh +++ b/hack/dev.sh @@ -2,6 +2,14 @@ set -euo pipefail +# Start local dev environment: Go backend + webpack dev server + console container. +# Prerequisites: oc login +# Optional: make setup-serverless (for full Knative/Serverless functionality) +# Usage: make dev | make dev-stop | make dev-randomize-ports + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/log.sh" + LOG_DIR=".dev-logs" CONSOLE_IMAGE="${CONSOLE_IMAGE:="quay.io/openshift/origin-console:latest"}" BACKEND_PORT=8080 @@ -21,15 +29,15 @@ wait_for_port() { local pid pid=$(cat "$pidfile") if ! kill -0 "$pid" 2>/dev/null; then - echo "Error: $label process exited. Check $LOG_DIR/ for details." + log::error "$label process exited. Check $LOG_DIR/ for details." exit 1 fi fi if [ $elapsed -ge $TIMEOUT ]; then - echo "Error: $label did not start within ${TIMEOUT}s. Check $LOG_DIR/ for details." + log::error "$label did not start within ${TIMEOUT}s. Check $LOG_DIR/ for details." exit 1 fi - echo "Waiting for $label (port $port)... ${elapsed}s" + log::waiting "Waiting for $label (port $port)... ${elapsed}s" sleep 1 elapsed=$((elapsed + 1)) done @@ -58,7 +66,7 @@ stop_pid() { if kill -0 "$pid" 2>/dev/null; then kill_tree "$pid" while kill -0 "$pid" 2>/dev/null; do sleep 0.1; done - echo "Stopped $label (PID $pid)." + log::info "Stopped $label (PID $pid)." fi rm -f "$pidfile" } @@ -89,45 +97,39 @@ build_pages() { echo "Error: helm not found. Install from https://helm.sh/docs/intro/install/" exit 1 fi - - local plugin_name="console-functions-plugin" - echo "Building pages assets..." - helm template "$plugin_name" charts/openshift-console-plugin \ - -n "$plugin_name" \ - --set "plugin.image=ghcr.io/functions-dev/${plugin_name}:latest" \ - --set "plugin.apiServerURL=$KUBE_API_SERVER" \ - > backend/static/plugin.yaml + log::info "Building pages assets..." + make manifests cp pages/index.html backend/static/index.html } extract_cluster_ca() { - echo "Extracting cluster CA certificate..." + log::info "Extracting cluster CA certificate..." CA_FILE=$(mktemp -t cluster-ca.XXXXXX).crt oc get cm kube-root-ca.crt -n default -o jsonpath='{.data.ca\.crt}' > "$CA_FILE" } resolve_kube_api_server() { - echo "Resolving cluster API server URL..." - KUBE_API_SERVER=$(oc whoami --show-server) + log::info "Resolving cluster API server URL..." + export KUBE_API_SERVER=$(oc whoami --show-server) } start_backend() { build_pages - echo "Building Go backend..." - (cd backend && go build -buildvcs=false -o ../bin/backend .) + log::info "Building Go backend..." + make build-backend (cd backend && go build -buildvcs=false -o ../bin/errserver ./cmd/errserver) - echo "Starting Go backend..." - ./bin/backend --http-port "$BACKEND_PORT" --kube-root-ca-path "$CA_FILE" --kube-host "$KUBE_API_SERVER" --external-api-server-url "$KUBE_API_SERVER" >>"$LOG_DIR/backend.log" 2>&1 & + log::info "Starting Go backend..." + ./bin/plugin-backend --http-port "$BACKEND_PORT" --kube-root-ca-path "$CA_FILE" --kube-host "$KUBE_API_SERVER" --external-api-server-url "$KUBE_API_SERVER" >>"$LOG_DIR/backend.log" 2>&1 & echo $! > "$PID_DIR/backend.pid" } start_backend_watcher() { if ! command -v inotifywait &>/dev/null; then - echo "Warning: inotifywait not found. Install inotify-tools for auto-recompile." + log::warn "inotifywait not found. Install inotify-tools for auto-recompile." return fi - echo "Starting backend file watcher..." + log::info "Starting backend file watcher..." ( while true; do if ! inotifywait -r -e modify,create,delete,move --include '\.(go|mod|sum)$' backend/ >/dev/null 2>&1; then @@ -139,7 +141,7 @@ start_backend_watcher() { echo "[watcher] Detected change, rebuilding backend..." old_pid=$(cat "$PID_DIR/backend.pid" 2>/dev/null || true) - build_output=$(cd backend && go build -buildvcs=false -o ../bin/backend-tmp . 2>&1) && build_ok=true || build_ok=false + build_output=$(cd backend && go build -buildvcs=false -o ../bin/plugin-backend-tmp . 2>&1) && build_ok=true || build_ok=false if [ -n "$old_pid" ]; then kill_tree "$old_pid" 2>/dev/null || true @@ -147,14 +149,14 @@ start_backend_watcher() { fi if $build_ok; then - mv bin/backend-tmp bin/backend - ./bin/backend --http-port "$BACKEND_PORT" --kube-root-ca-path "$CA_FILE" --kube-host "$KUBE_API_SERVER" --external-api-server-url "$KUBE_API_SERVER" >>"$LOG_DIR/backend.log" 2>&1 & + mv bin/plugin-backend-tmp bin/plugin-backend + ./bin/plugin-backend --http-port "$BACKEND_PORT" --kube-root-ca-path "$CA_FILE" --kube-host "$KUBE_API_SERVER" --external-api-server-url "$KUBE_API_SERVER" >>"$LOG_DIR/backend.log" 2>&1 & echo $! > "$PID_DIR/backend.pid" echo "[watcher] Backend restarted (PID $!)." else echo "[watcher] Build failed. Starting error server." echo "$build_output" - rm -f bin/backend-tmp + rm -f bin/plugin-backend-tmp echo "$build_output" > "$LOG_DIR/backend-build-error.txt" ./bin/errserver --port "$BACKEND_PORT" --msg-file "$LOG_DIR/backend-build-error.txt" >>"$LOG_DIR/backend.log" 2>&1 & errserver_pid=$! @@ -191,7 +193,7 @@ stop_console() { local cid cid=$(cat "$cidfile") if podman stop "$cid" >/dev/null 2>&1; then - echo "Stopped OpenShift console (container $cid)." + log::info "Stopped OpenShift console (container $cid)." fi rm -f "$cidfile" } @@ -205,33 +207,32 @@ stop_dev() { check_prerequisites() { if ! command -v oc &>/dev/null; then - echo "Error: oc CLI not found. Install from https://console.redhat.com/openshift/downloads" + log::error "oc CLI not found. Install from https://console.redhat.com/openshift/downloads" exit 1 fi if ! oc whoami &>/dev/null; then - echo "Error: not logged in to OpenShift. Run 'oc login' first." + log::error "Not logged in to OpenShift. Run 'oc login' first." exit 1 fi - } install_dependencies() { if [ ! -d "node_modules" ]; then - echo "Installing dependencies..." + log::info "Installing dependencies..." yarn install fi } start_plugin() { - echo "Starting plugin dev server..." + log::info "Starting plugin dev server..." PLUGIN_PORT="$PLUGIN_PORT" yarn start >"$LOG_DIR/webpack.log" 2>&1 & echo $! > "$PID_DIR/webpack.pid" } start_console() { - echo "Starting OpenShift console..." - ./start-console.sh \ + log::info "Starting OpenShift console..." + ./hack/start-console.sh \ --backend-port "$BACKEND_PORT" \ --plugin-port "$PLUGIN_PORT" \ --console-port "$CONSOLE_PORT" \ @@ -241,16 +242,16 @@ start_console() { } print_status() { - echo "" - echo "Dev environment started:" - echo " Backend: http://localhost:$BACKEND_PORT" - echo " Console: http://localhost:$CONSOLE_PORT" - echo " Logs: $LOG_DIR/" - echo "" - echo "To stop: ./init.sh --stop" + log::step "Dev environment started" + log::link "Backend" "http://localhost:$BACKEND_PORT" + log::link "Console" "http://localhost:$CONSOLE_PORT" + log::link "Logs" "$LOG_DIR/" + log::hint "To stop: make dev-stop" + log::hint "For full Knative integration: make setup-serverless" } main() { + log::step "Starting local dev environment" mkdir -p "$LOG_DIR" "$PID_DIR" bin check_prerequisites install_dependencies diff --git a/hack/lib/log.sh b/hack/lib/log.sh new file mode 100644 index 00000000..0186d352 --- /dev/null +++ b/hack/lib/log.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Shared logging functions for hack/ scripts. +# Source this file: source "$(dirname "${BASH_SOURCE[0]}")/lib/log.sh" + +_LOG_COLOR_RESET='\033[0m' +_LOG_COLOR_GREEN='\033[0;32m' +_LOG_COLOR_YELLOW='\033[0;33m' +_LOG_COLOR_RED='\033[0;31m' +_LOG_COLOR_CYAN='\033[0;36m' +_LOG_COLOR_BOLD='\033[1m' + +_log_script_name() { + basename "${BASH_SOURCE[2]:-${BASH_SOURCE[1]:-unknown}}" .sh +} + +log::step() { + local script + script=$(_log_script_name) + echo + echo -e "${_LOG_COLOR_BOLD}${_LOG_COLOR_CYAN}[${script}] $*${_LOG_COLOR_RESET}" + echo +} + +log::info() { + local script + script=$(_log_script_name) + echo -e "${_LOG_COLOR_GREEN}[${script}]${_LOG_COLOR_RESET} $*" +} + +log::warn() { + local script + script=$(_log_script_name) + echo -e "${_LOG_COLOR_YELLOW}[${script}] WARNING:${_LOG_COLOR_RESET} $*" >&2 +} + +log::error() { + local script + script=$(_log_script_name) + echo -e "${_LOG_COLOR_RED}[${script}] ERROR:${_LOG_COLOR_RESET} $*" >&2 +} + +log::waiting() { + local script + script=$(_log_script_name) + echo -e "[${script}] $*" +} + +log::link() { + local label="$1" url="$2" + echo -e " ${_LOG_COLOR_BOLD}${label}${_LOG_COLOR_RESET} \033[4m${url}${_LOG_COLOR_RESET}" +} + +log::hint() { + echo -e " $*" +} diff --git a/hack/setup-serverless.sh b/hack/setup-serverless.sh new file mode 100755 index 00000000..529f37a2 --- /dev/null +++ b/hack/setup-serverless.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Idempotent one-time cluster setup for Serverless operator and Knative Serving. +# Prerequisites: oc login with cluster-admin +# Usage: make setup-serverless + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/log.sh" + +SERVERLESS_NS="openshift-serverless" +SERVING_NS="knative-serving" +OPERATOR_NAME="serverless-operator" +CHANNEL="stable" +SOURCE="redhat-operators" +SOURCE_NS="openshift-marketplace" +TIMEOUT=300 + +# --- Prerequisites --- + +if ! command -v oc &>/dev/null; then + log::error "oc CLI not found. Install from https://console.redhat.com/openshift/downloads" + exit 1 +fi + +if ! oc whoami &>/dev/null; then + log::error "Not logged in to OpenShift. Run 'oc login' first." + exit 1 +fi + +# --- Serverless Operator --- + +log::step "Serverless Operator setup" + +if oc get namespace "$SERVERLESS_NS" &>/dev/null; then + log::info "Namespace $SERVERLESS_NS already exists." +else + log::info "Creating namespace $SERVERLESS_NS..." + oc create namespace "$SERVERLESS_NS" +fi + +log::info "Applying OperatorGroup..." +cat </dev/null || true) + if [ -n "$csv" ]; then + phase=$(oc get csv "$csv" -n "$SERVERLESS_NS" -o jsonpath='{.status.phase}' 2>/dev/null || true) + if [ "$phase" = "Succeeded" ]; then + log::info "CSV $csv is Succeeded." + break + fi + log::waiting "CSV $csv phase: $phase (${elapsed}s)..." + else + log::waiting "Waiting for CSV to appear (${elapsed}s)..." + fi + if [ "$elapsed" -ge "$TIMEOUT" ]; then + log::error "CSV did not reach Succeeded within ${TIMEOUT}s." + exit 1 + fi + sleep 10 + elapsed=$((elapsed + 10)) +done + +# --- Knative Serving --- + +log::step "Knative Serving setup" + +if oc get namespace "$SERVING_NS" &>/dev/null; then + log::info "Namespace $SERVING_NS already exists." +else + log::info "Creating namespace $SERVING_NS..." + oc create namespace "$SERVING_NS" +fi + +log::info "Applying KnativeServing CR..." +cat </dev/null || true) + if [ "$ready" = "True" ]; then + log::info "KnativeServing is Ready." + break + fi + if [ "$elapsed" -ge "$TIMEOUT" ]; then + log::error "KnativeServing did not become Ready within ${TIMEOUT}s." + exit 1 + fi + log::waiting "Waiting for KnativeServing ready (${elapsed}s)..." + sleep 10 + elapsed=$((elapsed + 10)) +done + +log::step "Cluster setup complete" diff --git a/start-console.sh b/hack/start-console.sh similarity index 92% rename from start-console.sh rename to hack/start-console.sh index c8099cb2..53d90011 100755 --- a/start-console.sh +++ b/hack/start-console.sh @@ -54,14 +54,8 @@ echo "Console Image: $CONSOLE_IMAGE" echo "Console URL: http://localhost:${CONSOLE_PORT}" echo "Console Platform: $CONSOLE_IMAGE_PLATFORM" -# Prefer podman if installed. Otherwise, fall back to docker. -if [ -x "$(command -v podman)" ]; then - CONTAINER_CMD="podman" - PLUGIN_HOST="host.containers.internal" -else - CONTAINER_CMD="docker" - PLUGIN_HOST="host.docker.internal" -fi +CONTAINER_CMD="podman" +PLUGIN_HOST="host.containers.internal" CONTAINER_NETWORK_OPTS="-p ${CONSOLE_PORT}:9000" if [[ "$BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT" == *"crc.testing"* ]]; then CONTAINER_NETWORK_OPTS="${CONTAINER_NETWORK_OPTS} --add-host api.crc.testing:host-gateway" diff --git a/hack/test-prow-e2e.sh b/hack/test-prow-e2e.sh new file mode 100755 index 00000000..c180ca06 --- /dev/null +++ b/hack/test-prow-e2e.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Prow e2e test entrypoint: deploys plugin to ephemeral cluster and runs Playwright. +# Called by: make e2e (CI only) +# Prerequisites: ci-operator cluster with PLUGIN_PULL_SPEC injected + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/log.sh" + +ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} +INSTALLER_DIR=${INSTALLER_DIR:=${ARTIFACT_DIR}/installer} + +function copyArtifacts { + if [ -d "$ARTIFACT_DIR" ]; then + log::info "Copying artifacts from $(pwd)..." + cp -r .e2e/results "${ARTIFACT_DIR}/" 2>/dev/null || true + cp -r .e2e/report "${ARTIFACT_DIR}/" 2>/dev/null || true + fi +} + +trap copyArtifacts EXIT + +# --- Credentials --- +BRIDGE_KUBEADMIN_PASSWORD="$(cat "${KUBEADMIN_PASSWORD_FILE:-${INSTALLER_DIR}/auth/kubeadmin-password}")" +export BRIDGE_KUBEADMIN_PASSWORD + +BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}')" +export BRIDGE_BASE_ADDRESS + +if [[ -z "${PLUGIN_PULL_SPEC:-}" ]]; then + log::error "PLUGIN_PULL_SPEC is not set. It should be injected by ci-operator as a dependency." + exit 1 +fi + +# --- Deploy plugin --- +log::step "Deploying plugin to cluster" + +make deploy IMAGE="${PLUGIN_PULL_SPEC}" + +# --- Install deps and run tests --- +log::step "Running e2e tests" + +log::info "Installing dependencies..." +make install-frontend + +log::info "Installing Playwright browsers..." +npx playwright install chromium + +log::info "Running Playwright e2e tests..." +make test-e2e diff --git a/package.json b/package.json index 1698531c..b68791b6 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,13 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/functions-dev/ocp-console-plugin.git" + "url": "https://github.com/openshift/faas-console-plugin.git" }, "scripts": { "clean": "rm -rf dist", "build": "yarn clean && NODE_ENV=production yarn webpack", "build-dev": "yarn clean && yarn webpack", "start": "yarn webpack serve --progress", - "start-console": "./start-console.sh", "test": "vitest run", "i18n": "i18next-cli extract", "lint": "yarn eslint src e2e && stylelint 'src/**/*.css' --allow-empty-input", diff --git a/pages/index.html b/pages/index.html index 5c19f942..bdfc5711 100644 --- a/pages/index.html +++ b/pages/index.html @@ -201,7 +201,7 @@
Install
Copy failed
$ oc new-project console-functions-plugin
-$ oc apply -f https://functions-dev.github.io/ocp-console-plugin/plugin.yaml
+$ oc apply -f backend/static/plugin.yaml @@ -209,8 +209,8 @@
Install
Resources
diff --git a/test-frontend.sh b/test-frontend.sh deleted file mode 100755 index b6be95cb..00000000 --- a/test-frontend.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# https://ci-operator-configresolver-ui-ci.apps.ci.l2s4.p1.openshiftapps.com/help#env -OPENSHIFT_CI=${OPENSHIFT_CI:=false} -ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} - -yarn i18n -GIT_STATUS="$(git status --short --untracked-files -- locales)" -if [ -n "$GIT_STATUS" ]; then - echo "i18n files are not up to date. Run 'yarn i18n' then commit changes." - git --no-pager diff - exit 1 -fi - -if ! yarn dedupe --strategy highest --check ; then - echo "You have duplicate version resolutions of some packages in yarn.lock. Run 'yarn dedupe' and commit the updated yarn.lock." - yarn dedupe --strategy highest - git --no-pager diff - exit 1 -fi diff --git a/test-prow-e2e.sh b/test-prow-e2e.sh deleted file mode 100755 index 0123b360..00000000 --- a/test-prow-e2e.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash - -set -exuo pipefail - -ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} -INSTALLER_DIR=${INSTALLER_DIR:=${ARTIFACT_DIR}/installer} -PLUGIN_NAME="console-functions-plugin" -PLUGIN_NAMESPACE="console-functions-plugin" - -function copyArtifacts { - if [ -d "$ARTIFACT_DIR" ]; then - echo "Copying artifacts from $(pwd)..." - cp -r .e2e/results "${ARTIFACT_DIR}/" 2>/dev/null || true - cp -r .e2e/report "${ARTIFACT_DIR}/" 2>/dev/null || true - fi -} - -trap copyArtifacts EXIT - -# --- Credentials --- -set +x -BRIDGE_KUBEADMIN_PASSWORD="$(cat "${KUBEADMIN_PASSWORD_FILE:-${INSTALLER_DIR}/auth/kubeadmin-password}")" -export BRIDGE_KUBEADMIN_PASSWORD -set -x - -BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}')" -export BRIDGE_BASE_ADDRESS - -echo "Console URL: ${BRIDGE_BASE_ADDRESS}" - -if [[ -z "${PLUGIN_PULL_SPEC:-}" ]]; then - echo "Error: PLUGIN_PULL_SPEC is not set. It should be injected by ci-operator as a dependency." - exit 1 -fi - -# --- Install Helm --- -echo "Installing Helm..." -HELM_DIR=$(mktemp -d) -export PATH="$HELM_DIR:$PATH" -curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | HELM_INSTALL_DIR="$HELM_DIR" bash -s -- --no-sudo --version v3.21.3 - -# --- Deploy plugin --- -oc new-project "${PLUGIN_NAMESPACE}" || oc project "${PLUGIN_NAMESPACE}" - -helm install "${PLUGIN_NAME}" charts/openshift-console-plugin \ - -n "${PLUGIN_NAMESPACE}" \ - --set "plugin.image=${PLUGIN_PULL_SPEC}" \ - --set "plugin.name=${PLUGIN_NAME}" - -echo "Waiting for plugin deployment rollout..." -oc rollout status deployment/"${PLUGIN_NAME}" -n "${PLUGIN_NAMESPACE}" --timeout=300s - -echo "Waiting for ConsolePlugin CR..." -for i in $(seq 1 60); do - if oc get consoleplugins "${PLUGIN_NAME}" &>/dev/null; then - echo "ConsolePlugin CR found." - break - fi - if [ "$i" -eq 60 ]; then - echo "Error: ConsolePlugin CR did not appear within 120s." - oc get all -n "${PLUGIN_NAMESPACE}" - exit 1 - fi - sleep 2 -done - -echo "Enabling plugin on the console..." -oc patch consoles.operator.openshift.io cluster \ - --type=json \ - --patch='[{"op":"add","path":"/spec/plugins/-","value":"'"${PLUGIN_NAME}"'"}]' - -echo "Restarting console pods to pick up the plugin..." -oc rollout restart deployment/console -n openshift-console -oc rollout status deployment/console -n openshift-console --timeout=300s - -# --- Install deps and run tests --- -echo "Installing dependencies..." -yarn install --immutable - -echo "Installing Playwright browsers..." -npx playwright install --with-deps chromium - -echo "Running Playwright e2e tests..." -yarn test:e2e