diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dd4184d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +# Repository metadata +.git +.github/ +.vscode/ + +# Dependencies and generated output +node_modules/ +build/ +dist/ +coverage/ +*.tar.gz +.eslintcache + +# Local-only files +.env +*.log + +# Tests, documentation, and image assets +*.md +tests/ +docs/ +screenshots/ +img/ +.DS_Store diff --git a/.github/workflows/release-plugin-oci.yaml b/.github/workflows/release-plugin-oci.yaml new file mode 100644 index 0000000..08b64e3 --- /dev/null +++ b/.github/workflows/release-plugin-oci.yaml @@ -0,0 +1,157 @@ +name: Build and publish plugin container image + +on: + # Automatic path: when release-please publishes a vX.Y.Z release, + # build the tagged plugin source and publish its OCI image to GHCR. + release: + types: [published] + # Manual path: retry a failed run (or cut a release without release-please) + # by providing an existing tag such as v0.1.0. + workflow_dispatch: + inputs: + tag: + description: 'Existing release tag to (re)build, e.g. v0.1.0' + required: true + type: string + +permissions: + contents: read + +env: + REGISTRY: ghcr.io + ORG: ${{ github.repository_owner }} + PLUGIN: cnpg-headlamp-plugin + FULL_IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/cnpg-headlamp-plugin +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write # needed for publishing the container image + attestations: write + id-token: write + steps: + - name: Checkout release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + with: + ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 + with: + node-version: 24 + cache: npm + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 #v4.4.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 #v4.4.1 + + - name: Resolve version + id: version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + else + TAG="${{ github.event.inputs.tag }}" + fi + if [ -z "$TAG" ]; then + echo "Error: no tag resolved (release.tag_name or inputs.tag)" >&2 + exit 1 + fi + if [ "${TAG#v}" = "$TAG" ]; then + echo "Error: tag must be v-prefixed, got '$TAG'" >&2 + exit 1 + fi + # Tags are v-prefixed (v0.1.0); the plugin version is not (0.1.0). + VERSION="${TAG#v}" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "TAG=$TAG" >> $GITHUB_ENV + echo "IMAGE_TAG_VERSION=$TAG" >> $GITHUB_ENV + + - name: Docker meta + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 #v6.2.0 + env: + # Attach annotations to both manifests and the multi-arch index + # so they are visible on GHCR, following cloudnative-pg/docker-bake.hcl + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=raw,value=${{ env.IMAGE_TAG_VERSION }} + # version, revision, created, source are auto-generated by + # metadata-action. We only pin the stable descriptive fields here, + # mirroring cloudnative-pg/docker-bake.hcl. + labels: | + org.opencontainers.image.title=CloudNativePG Headlamp Plugin + org.opencontainers.image.description=Headlamp plugin for managing CloudNativePG resources - directly from the Headlamp + org.opencontainers.image.vendor=CloudNativePG + org.opencontainers.image.authors=The CloudNativePG Contributors + org.opencontainers.image.licenses=Apache-2.0 + org.opencontainers.image.documentation=https://cloudnative-pg.io/documentation/ + org.opencontainers.image.url=https://github.com/cloudnative-pg/cnpg-headlamp-plugin + # metadata-action does not propagate custom labels to annotations + # automatically, so mirror them explicitly. + annotations: | + org.opencontainers.image.title=CloudNativePG Headlamp Plugin + org.opencontainers.image.description=Headlamp plugin for managing CloudNativePG resources - directly from the Headlamp + org.opencontainers.image.vendor=CloudNativePG + org.opencontainers.image.authors=The CloudNativePG Contributors + org.opencontainers.image.licenses=Apache-2.0 + org.opencontainers.image.documentation=https://cloudnative-pg.io/documentation/ + org.opencontainers.image.url=https://github.com/cloudnative-pg/cnpg-headlamp-plugin + + - name: Log in to the Container registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f #v4.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ github.token }} + + - name: Build Docker image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc #v7.4.0 + timeout-minutes: 15 + with: + context: . + push: false + tags: ${{ steps.meta.outputs.tags }} + load: true + build-args: PLUGIN=${{ env.PLUGIN }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Test + run: | + docker run --rm ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG_VERSION }} sh -c "ls /plugins/${{ env.PLUGIN }}" + + - name: Push Docker image + id: push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc #v7.4.0 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + labels: ${{ steps.meta.outputs.labels }} + provenance: true + build-args: PLUGIN=${{ env.PLUGIN }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Summary + run: | + echo "## Container Image Published" >> $GITHUB_STEP_SUMMARY + echo "Plugin: ${{ env.PLUGIN }}" >> $GITHUB_STEP_SUMMARY + echo "Version: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "Image: ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG_VERSION }}" >> $GITHUB_STEP_SUMMARY + + - name: Generate artifact attestation + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 #v4.2.2 + with: + subject-name: ${{ env.FULL_IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a205a95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +FROM node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 AS builder + +# Set the working directory inside the container +WORKDIR /headlamp-plugins + +# Add a build argument for the desired plugin to be built +ARG PLUGIN + +# Check if the PLUGIN argument is provided +RUN if [ -z "$PLUGIN" ]; then \ + echo "Error: PLUGIN argument is required"; \ + exit 1; \ + fi + +# Remove any existing node_modules and package-lock.json to avoid arch-specific conflicts +COPY package.json package-lock.json /headlamp-plugins/${PLUGIN}/ + +# Install dependencies for the specified plugin +RUN --mount=type=cache,target=/root/.npm \ + echo "Installing deps for plugin $PLUGIN..."; \ + cd /headlamp-plugins/${PLUGIN}; \ + npm ci + +# Create a directory for the plugin build +COPY . /headlamp-plugins/${PLUGIN} + +# Build the specified plugin +RUN echo "Building plugin $PLUGIN..."; \ + cd /headlamp-plugins/${PLUGIN}; \ + npm run build + +# Extract the built plugin to the build directory +RUN mkdir -p /headlamp-plugins/build && \ + echo "Extracting plugin ${PLUGIN}..." && \ + cd "/headlamp-plugins/${PLUGIN}" && \ + npx --no-install headlamp-plugin extract . "/headlamp-plugins/build/${PLUGIN}" + +FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 AS final + +# Create a non-root user and group +RUN addgroup -S headlamp && adduser -S headlamp -G headlamp + +# Copy the built plugin files from the builder stage to the /plugins directory in the final image +COPY --from=builder /headlamp-plugins/build/ /plugins/ + +# Set appropriate permissions for the plugins directory +RUN chown -R headlamp:headlamp /plugins && \ + chmod -R 755 /plugins + +# Switch to non-root user +USER headlamp + +# Set the default command to list the installed plugins +CMD ["sh", "-c", "echo Plugins installed at /plugins/:; ls /plugins/"] diff --git a/README.md b/README.md index 8fe37d8..810cd0c 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,19 @@ List and detail views for managing available Postgres operand images. 1. Download the `cnpg-headlamp-plugin-.tar.gz` archive from the [GitHub Releases](https://github.com/cloudnative-pg/cnpg-headlamp-plugin/releases) page 2. Install it following the [Headlamp plugin installation documentation](https://headlamp.dev/docs/latest/development/plugins/) +### In-Cluster install + +Deploy Headlamp together with the plugin using the example manifest in [`deploy/headlamp-with-cnpg.yaml`](deploy/headlamp-with-cnpg.yaml), replacing `vX.Y.Z` with a [released plugin version](https://github.com/cloudnative-pg/cnpg-headlamp-plugin/releases): + +```sh +VERSION=vX.Y.Z # e.g. VERSION=v0.1.4 +curl -s https://raw.githubusercontent.com/cloudnative-pg/cnpg-headlamp-plugin/main/deploy/headlamp-with-cnpg.yaml \ + | sed "s/vX.Y.Z/$VERSION/" \ + | kubectl apply -f - +``` + +Note: with `-in-cluster`, Headlamp authenticates with its own ServiceAccount, so grant it the RBAC permissions described in the [Headlamp in-cluster documentation](https://headlamp.dev/docs/latest/installation/in-cluster/) and generate a login token for your user (`kubectl create token `). + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the live-load diff --git a/deploy/headlamp-with-cnpg.yaml b/deploy/headlamp-with-cnpg.yaml new file mode 100644 index 0000000..020c42f --- /dev/null +++ b/deploy/headlamp-with-cnpg.yaml @@ -0,0 +1,56 @@ +# Example: run Headlamp in-cluster with the CNPG plugin installed. +# +# Replace vX.Y.Z below with a released plugin version, e.g. v0.1.4: +# https://github.com/cloudnative-pg/cnpg-headlamp-plugin/releases +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp-with-cnpg + labels: + app: headlamp-with-cnpg +spec: + selector: + matchLabels: + app: headlamp-with-cnpg + template: + metadata: + labels: + app: headlamp-with-cnpg + spec: + initContainers: + - name: fetch-plugins + image: ghcr.io/cloudnative-pg/cnpg-headlamp-plugin:vX.Y.Z + # Copy the plugins into the shared volume before Headlamp starts. + command: ["/bin/sh", "-c"] + args: ["cp -r /plugins/* /headlamp/plugins/ && ls -l /headlamp/plugins"] + volumeMounts: + - name: plugins + mountPath: /headlamp/plugins + containers: + - name: headlamp + image: ghcr.io/headlamp-k8s/headlamp:latest + args: + - -plugins-dir=/headlamp/plugins + - -in-cluster + ports: + - containerPort: 4466 + volumeMounts: + - name: plugins + mountPath: /headlamp/plugins + volumes: + - name: plugins + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: headlamp-with-cnpg + labels: + app: headlamp-with-cnpg +spec: + selector: + app: headlamp-with-cnpg + ports: + - name: http + port: 4466 + targetPort: 4466