Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
157 changes: 157 additions & 0 deletions .github/workflows/release-plugin-oci.yaml
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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/"]
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ List and detail views for managing available Postgres operand images.
1. Download the `cnpg-headlamp-plugin-<version>.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 <serviceaccount>`).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the live-load
Expand Down
56 changes: 56 additions & 0 deletions deploy/headlamp-with-cnpg.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading