mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Add Helm chart tooling, CI, and release packaging
This commit is contained in:
parent
d79b8e8883
commit
d15ad1d0b4
25 changed files with 1299 additions and 5 deletions
31
.github/workflows/README.md
vendored
31
.github/workflows/README.md
vendored
|
|
@ -49,3 +49,34 @@ ssh pulse-relay "curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/ma
|
|||
- ✅ Validates install script works on real server
|
||||
- ✅ Removes manual step from release process
|
||||
- ✅ Free to run (public repos get unlimited GitHub Actions minutes)
|
||||
|
||||
## Helm CI
|
||||
|
||||
**File**: `helm-ci.yml`
|
||||
|
||||
Runs `helm lint --strict` and renders the chart with common configuration combinations on every pull request that touches Helm content (and on pushes to `main`). This prevents regressions before they land.
|
||||
|
||||
- Triggered by PRs/pushes touching `deploy/helm/**`, docs, or the workflow itself
|
||||
- Uses Helm v3.15.2
|
||||
- Renders both the default deployment and an agent-enabled configuration to catch template issues
|
||||
|
||||
## Publish Helm Chart
|
||||
|
||||
**File**: `publish-helm-chart.yml`
|
||||
|
||||
Packages the Helm chart and pushes it to the GitHub Container Registry (OCI) whenever a GitHub Release is published. Also makes the packaged `.tgz` available as both an Actions artifact and a release asset. The same behaviour can be triggered locally via `./scripts/package-helm-chart.sh <version> [--push]`.
|
||||
|
||||
- Triggered automatically on `release: published`, or manually via workflow dispatch (requires `chart_version` input)
|
||||
- Chart and app versions mirror the Pulse release tag (e.g., `v4.24.0` → `4.24.0`)
|
||||
- Publishes to `oci://ghcr.io/<owner>/pulse-chart`
|
||||
- Requires no additional secrets—uses the built-in `GITHUB_TOKEN` with `packages: write` permission
|
||||
|
||||
## Helm Integration (Kind)
|
||||
|
||||
**File**: `helm-integration.yml`
|
||||
|
||||
Creates a disposable Kind cluster, installs the chart, waits for the hub deployment to report ready, and performs a `/health` smoke check from inside the cluster.
|
||||
|
||||
- Triggered alongside the lint workflow for PRs/pushes touching Helm content
|
||||
- Disables persistence to keep the Kind cluster lightweight
|
||||
- Provides early detection of runtime issues (missing secrets, invalid probes, etc.)
|
||||
|
|
|
|||
48
.github/workflows/helm-ci.yml
vendored
Normal file
48
.github/workflows/helm-ci.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Helm CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "deploy/helm/**"
|
||||
- ".github/workflows/helm-ci.yml"
|
||||
- "docs/KUBERNETES.md"
|
||||
- "README.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- "deploy/helm/**"
|
||||
- ".github/workflows/helm-ci.yml"
|
||||
- "docs/KUBERNETES.md"
|
||||
- "README.md"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint and Render Chart
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.15.2
|
||||
|
||||
- name: Helm lint (strict)
|
||||
run: helm lint deploy/helm/pulse --strict
|
||||
|
||||
- name: Render default manifests
|
||||
run: helm template pulse deploy/helm/pulse > /tmp/pulse-rendered.yaml
|
||||
|
||||
- name: Render agent-enabled manifests
|
||||
run: |
|
||||
helm template pulse deploy/helm/pulse \
|
||||
--set agent.enabled=true \
|
||||
--set agent.kind=Deployment \
|
||||
--set agent.secretEnv.create=true \
|
||||
--set agent.secretEnv.data.PULSE_TOKEN=dummy-token \
|
||||
--set server.secretEnv.create=true \
|
||||
--set server.secretEnv.data.API_TOKENS=dummy-token \
|
||||
--set persistence.enabled=false \
|
||||
> /tmp/pulse-agent-rendered.yaml
|
||||
58
.github/workflows/helm-integration.yml
vendored
Normal file
58
.github/workflows/helm-integration.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: Helm Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "deploy/helm/**"
|
||||
- ".github/workflows/helm-*.yml"
|
||||
- "docs/KUBERNETES.md"
|
||||
- "README.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- "deploy/helm/**"
|
||||
- ".github/workflows/helm-*.yml"
|
||||
- "docs/KUBERNETES.md"
|
||||
- "README.md"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
kind-smoke-test:
|
||||
name: Deploy to Kind and Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.15.2
|
||||
|
||||
- name: Create Kind cluster
|
||||
uses: helm/kind-action@v1.8.0
|
||||
with:
|
||||
wait: 120s
|
||||
|
||||
- name: Install Pulse chart
|
||||
run: |
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
--create-namespace \
|
||||
--set persistence.enabled=false \
|
||||
--set server.secretEnv.create=true \
|
||||
--set server.secretEnv.data.API_TOKENS=dummy-token \
|
||||
--wait \
|
||||
--timeout 5m
|
||||
|
||||
- name: Verify deployment is available
|
||||
run: kubectl -n pulse wait --for=condition=available deployment/pulse --timeout=120s
|
||||
|
||||
- name: Hit health endpoint from inside the cluster
|
||||
run: |
|
||||
kubectl -n pulse run smoke-test \
|
||||
--rm \
|
||||
--image=curlimages/curl:8.3.0 \
|
||||
--restart=Never \
|
||||
-- curl -fsS http://pulse:7655/health
|
||||
92
.github/workflows/publish-helm-chart.yml
vendored
Normal file
92
.github/workflows/publish-helm-chart.yml
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
name: Publish Helm Chart
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
chart_version:
|
||||
description: "Chart version (required when running manually, use format 4.24.0)"
|
||||
required: true
|
||||
app_version:
|
||||
description: "Application version to embed (defaults to chart version)"
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Package and Push Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.15.2
|
||||
|
||||
- name: Determine chart version
|
||||
id: versions
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
CHART_VERSION="${{ inputs.chart_version }}"
|
||||
if [ -z "$CHART_VERSION" ]; then
|
||||
echo "::error::chart_version input is required when running manually"
|
||||
exit 1
|
||||
fi
|
||||
APP_VERSION="${{ inputs.app_version }}"
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$CHART_VERSION"
|
||||
fi
|
||||
RELEASE_TAG="$CHART_VERSION"
|
||||
else
|
||||
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
||||
if [ -z "$RELEASE_TAG" ]; then
|
||||
echo "::error::Release tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
CHART_VERSION="${RELEASE_TAG#v}"
|
||||
APP_VERSION="$CHART_VERSION"
|
||||
fi
|
||||
|
||||
echo "chart_version=$CHART_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "app_version=$APP_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Helm lint (strict)
|
||||
run: helm lint deploy/helm/pulse --strict
|
||||
|
||||
- name: Package chart
|
||||
run: |
|
||||
mkdir -p dist
|
||||
helm package deploy/helm/pulse \
|
||||
--version "${{ steps.versions.outputs.chart_version }}" \
|
||||
--app-version "${{ steps.versions.outputs.app_version }}" \
|
||||
--destination dist
|
||||
|
||||
- name: Upload packaged chart artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pulse-chart-${{ steps.versions.outputs.chart_version }}
|
||||
path: dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz
|
||||
|
||||
- name: Authenticate with GHCR
|
||||
run: |
|
||||
echo "${{ github.token }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin
|
||||
|
||||
- name: Push chart to GHCR
|
||||
run: |
|
||||
helm push dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz \
|
||||
oci://ghcr.io/${{ github.repository_owner }}/pulse-chart
|
||||
|
||||
- name: Attach chart to release
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release upload "${{ steps.versions.outputs.release_tag }}" \
|
||||
dist/pulse-${{ steps.versions.outputs.chart_version }}.tgz \
|
||||
--clobber
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Binaries
|
||||
/bin/
|
||||
pulse
|
||||
/pulse
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -84,6 +84,14 @@ docker run -d -p 7655:7655 -v pulse_data:/data rcourtman/pulse:latest
|
|||
|
||||
# Testing: Install from main branch source (for testing latest fixes)
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash -s -- --main
|
||||
|
||||
# Alternative: Kubernetes (Helm)
|
||||
helm registry login ghcr.io
|
||||
helm install pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--version $(curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/VERSION) \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
# Replace the VERSION lookup with a specific release if you need to pin. For local development, see docs/KUBERNETES.md.
|
||||
```
|
||||
|
||||
**Proxmox users**: The installer detects PVE hosts and automatically creates an optimized LXC container. Choose Quick mode for one-minute setup.
|
||||
|
|
|
|||
17
deploy/helm/pulse/Chart.yaml
Normal file
17
deploy/helm/pulse/Chart.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
apiVersion: v2
|
||||
name: pulse
|
||||
description: Helm chart for deploying the Pulse hub and optional Docker monitoring agent.
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "4.24.0"
|
||||
icon: https://raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg
|
||||
keywords:
|
||||
- monitoring
|
||||
- proxmox
|
||||
- observability
|
||||
home: https://github.com/rcourtman/Pulse
|
||||
sources:
|
||||
- https://github.com/rcourtman/Pulse
|
||||
maintainers:
|
||||
- name: Pulse Maintainers
|
||||
email: pulse@rcourtman.dev
|
||||
41
deploy/helm/pulse/README.md
Normal file
41
deploy/helm/pulse/README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Pulse Helm Chart
|
||||
|
||||
This chart deploys the Pulse hub (web UI + API) and, optionally, the Docker monitoring agent.
|
||||
|
||||
## Installing from GHCR
|
||||
|
||||
```bash
|
||||
helm registry login ghcr.io
|
||||
helm install pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--version $(curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/VERSION) \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
Replace the inline `curl` command if you need to pin to a specific release. The chart version tracks the Pulse release version (e.g., Pulse `v4.24.0` → chart `4.24.0`).
|
||||
|
||||
## Common Values
|
||||
|
||||
| Key | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `persistence.enabled` | Persist /data using a PVC. Disable for ephemeral testing. | `true` |
|
||||
| `ingress.enabled` | Create an Ingress resource. Configure hosts/TLS via `ingress.hosts` and `ingress.tls`. | `false` |
|
||||
| `server.secretEnv` | Manage sensitive env vars (API tokens, auth secrets). Enable `create` or point at an existing secret. | `{}` |
|
||||
| `agent.enabled` | Deploy the Docker agent as a DaemonSet or Deployment. Requires access to the Docker socket by default. | `false` |
|
||||
|
||||
See `values.yaml` for the full list of overridable settings.
|
||||
|
||||
## Local Development
|
||||
|
||||
Install from the working copy when testing chart changes:
|
||||
|
||||
```bash
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
--create-namespace \
|
||||
--set persistence.enabled=false \
|
||||
--set server.secretEnv.create=true \
|
||||
--set server.secretEnv.data.API_TOKENS=dummy-token
|
||||
```
|
||||
|
||||
For more deployment details (ingress, agent, persistence options), refer to `docs/KUBERNETES.md`.
|
||||
4
deploy/helm/pulse/templates/NOTES.txt
Normal file
4
deploy/helm/pulse/templates/NOTES.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
1. Pulse hub URL: http://{{ include "pulse.fullname" . }}:{{ .Values.service.port }}
|
||||
(Adjust if using ingress or LoadBalancer.)
|
||||
2. Create an API token from the Pulse UI under Settings → Security for the Docker agent.
|
||||
3. Deploy the optional agent by enabling `agent.enabled` and providing your server URL and token.
|
||||
97
deploy/helm/pulse/templates/_helpers.tpl
Normal file
97
deploy/helm/pulse/templates/_helpers.tpl
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "pulse.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "pulse.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart label.
|
||||
*/}}
|
||||
{{- define "pulse.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels.
|
||||
*/}}
|
||||
{{- define "pulse.labels" -}}
|
||||
helm.sh/chart: {{ include "pulse.chart" . }}
|
||||
{{ include "pulse.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels.
|
||||
*/}}
|
||||
{{- define "pulse.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "pulse.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the name of the service account to use.
|
||||
*/}}
|
||||
{{- define "pulse.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{- default (printf "%s-sa" (include "pulse.fullname" .)) .Values.serviceAccount.name -}}
|
||||
{{- else -}}
|
||||
{{- default "default" .Values.serviceAccount.name -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the server secret name (Pulse hub env vars).
|
||||
*/}}
|
||||
{{- define "pulse.serverSecretName" -}}
|
||||
{{- $secret := .Values.server.secretEnv -}}
|
||||
{{- if $secret.name -}}
|
||||
{{- $secret.name -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-server-env" (include "pulse.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the agent secret name.
|
||||
*/}}
|
||||
{{- define "pulse.agentSecretName" -}}
|
||||
{{- $secret := .Values.agent.secretEnv -}}
|
||||
{{- if $secret.name -}}
|
||||
{{- $secret.name -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-agent-env" (include "pulse.fullname" .) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the agent service account name.
|
||||
*/}}
|
||||
{{- define "pulse.agentServiceAccountName" -}}
|
||||
{{- if .Values.agent.serviceAccount.create -}}
|
||||
{{- default (printf "%s-agent" (include "pulse.fullname" .)) .Values.agent.serviceAccount.name -}}
|
||||
{{- else if .Values.agent.serviceAccount.name -}}
|
||||
{{- .Values.agent.serviceAccount.name -}}
|
||||
{{- else -}}
|
||||
{{- include "pulse.serviceAccountName" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
14
deploy/helm/pulse/templates/agent-secret.yaml
Normal file
14
deploy/helm/pulse/templates/agent-secret.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{{- $secret := .Values.agent.secretEnv }}
|
||||
{{- if and .Values.agent.enabled $secret.create (gt (len $secret.data) 0) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "pulse.agentSecretName" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $key, $value := $secret.data }}
|
||||
{{ $key }}: {{ $value | toString | b64enc }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
12
deploy/helm/pulse/templates/agent-serviceaccount.yaml
Normal file
12
deploy/helm/pulse/templates/agent-serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{{- if and .Values.agent.enabled .Values.agent.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ default (printf "%s-agent" (include "pulse.fullname" .)) .Values.agent.serviceAccount.name }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
{{- with .Values.agent.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
125
deploy/helm/pulse/templates/agent.yaml
Normal file
125
deploy/helm/pulse/templates/agent.yaml
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{{- if .Values.agent.enabled }}
|
||||
{{- $kind := default "DaemonSet" .Values.agent.kind | lower }}
|
||||
{{- $isDaemon := eq $kind "daemonset" }}
|
||||
{{- $workloadName := printf "%s-agent" (include "pulse.fullname" .) }}
|
||||
apiVersion: apps/v1
|
||||
kind: {{ if $isDaemon }}DaemonSet{{ else }}Deployment{{ end }}
|
||||
metadata:
|
||||
name: {{ $workloadName }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: agent
|
||||
spec:
|
||||
{{- if not $isDaemon }}
|
||||
replicas: {{ .Values.agent.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "pulse.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: agent
|
||||
{{- if $isDaemon }}
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
{{- end }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "pulse.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: agent
|
||||
{{- with .Values.agent.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.agent.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml .Values.agent.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "pulse.agentServiceAccountName" . }}
|
||||
{{- $podSecurityContext := .Values.agent.podSecurityContext }}
|
||||
{{- if $podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml $podSecurityContext | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: pulse-docker-agent
|
||||
image: "{{ .Values.agent.image.repository }}:{{ default .Chart.AppVersion .Values.agent.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.agent.image.pullPolicy }}
|
||||
{{- if .Values.agent.args }}
|
||||
args:
|
||||
{{- toYaml .Values.agent.args | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $csec := .Values.agent.securityContext }}
|
||||
{{- if $csec }}
|
||||
securityContext:
|
||||
{{- toYaml $csec | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $envList := list }}
|
||||
{{- range .Values.agent.env }}
|
||||
{{- $envList = append $envList . }}
|
||||
{{- end }}
|
||||
{{- range .Values.agent.extraEnv }}
|
||||
{{- $envList = append $envList . }}
|
||||
{{- end }}
|
||||
{{- $secret := .Values.agent.secretEnv }}
|
||||
{{- $secretKeys := list }}
|
||||
{{- if $secret.keys }}
|
||||
{{- $secretKeys = $secret.keys }}
|
||||
{{- else if $secret.data }}
|
||||
{{- $secretKeys = keys $secret.data }}
|
||||
{{- end }}
|
||||
{{- $root := . }}
|
||||
{{- range $key := $secretKeys }}
|
||||
{{- $envList = append $envList (dict "name" $key "valueFrom" (dict "secretKeyRef" (dict "name" (include "pulse.agentSecretName" $root) "key" $key))) }}
|
||||
{{- end }}
|
||||
{{- if $envList }}
|
||||
env:
|
||||
{{- toYaml $envList | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $envFrom := concat .Values.agent.envFrom .Values.agent.extraEnvFrom }}
|
||||
{{- if $envFrom }}
|
||||
envFrom:
|
||||
{{- range $envFrom }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if or .Values.agent.dockerSocket.enabled .Values.agent.extraVolumeMounts }}
|
||||
volumeMounts:
|
||||
{{- if .Values.agent.dockerSocket.enabled }}
|
||||
- name: docker-socket
|
||||
mountPath: {{ .Values.agent.dockerSocket.path }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.extraVolumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if or .Values.agent.dockerSocket.enabled .Values.agent.extraVolumes }}
|
||||
volumes:
|
||||
{{- if .Values.agent.dockerSocket.enabled }}
|
||||
- name: docker-socket
|
||||
hostPath:
|
||||
path: {{ .Values.agent.dockerSocket.path }}
|
||||
{{- with .Values.agent.dockerSocket.hostPathType }}
|
||||
type: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.agent.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
135
deploy/helm/pulse/templates/deployment.yaml
Normal file
135
deploy/helm/pulse/templates/deployment.yaml
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "pulse.fullname" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "pulse.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "pulse.selectorLabels" . | nindent 8 }}
|
||||
{{- with .Values.server.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- $podAnnotations := merge (dict) .Values.podAnnotations .Values.server.podAnnotations }}
|
||||
{{- if $podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml $podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "pulse.serviceAccountName" . }}
|
||||
{{- $podSecurityContext := merge (dict) .Values.podSecurityContext .Values.server.podSecurityContext }}
|
||||
{{- if $podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml $podSecurityContext | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: pulse
|
||||
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 7655
|
||||
{{- $containerSecurityContext := merge (dict) .Values.securityContext .Values.server.securityContext }}
|
||||
{{- if $containerSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml $containerSecurityContext | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $envList := list }}
|
||||
{{- range .Values.server.env }}
|
||||
{{- $envList = append $envList . }}
|
||||
{{- end }}
|
||||
{{- range .Values.server.extraEnv }}
|
||||
{{- $envList = append $envList . }}
|
||||
{{- end }}
|
||||
{{- $secret := .Values.server.secretEnv }}
|
||||
{{- $secretKeys := list }}
|
||||
{{- if $secret.keys }}
|
||||
{{- $secretKeys = $secret.keys }}
|
||||
{{- else if $secret.data }}
|
||||
{{- $secretKeys = keys $secret.data }}
|
||||
{{- end }}
|
||||
{{- $root := . }}
|
||||
{{- range $key := $secretKeys }}
|
||||
{{- $envList = append $envList (dict "name" $key "valueFrom" (dict "secretKeyRef" (dict "name" (include "pulse.serverSecretName" $root) "key" $key))) }}
|
||||
{{- end }}
|
||||
{{- if $envList }}
|
||||
env:
|
||||
{{- toYaml $envList | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- $envFrom := concat .Values.server.envFrom .Values.server.extraEnvFrom }}
|
||||
{{- if $envFrom }}
|
||||
envFrom:
|
||||
{{- range $envFrom }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- with .Values.server.extraVolumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.livenessProbe }}
|
||||
{{- if .enabled }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ .path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .initialDelaySeconds }}
|
||||
periodSeconds: {{ .periodSeconds }}
|
||||
timeoutSeconds: {{ .timeoutSeconds }}
|
||||
failureThreshold: {{ .failureThreshold }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.readinessProbe }}
|
||||
{{- if .enabled }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ .path }}
|
||||
port: http
|
||||
initialDelaySeconds: {{ .initialDelaySeconds }}
|
||||
periodSeconds: {{ .periodSeconds }}
|
||||
timeoutSeconds: {{ .timeoutSeconds }}
|
||||
failureThreshold: {{ .failureThreshold }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "pulse.fullname" . }}
|
||||
{{- else if and .Values.persistence.enabled .Values.persistence.existingClaim }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.existingClaim }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- with .Values.server.extraVolumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.server.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
35
deploy/helm/pulse/templates/ingress.yaml
Normal file
35
deploy/helm/pulse/templates/ingress.yaml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "pulse.fullname" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "pulse.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml .Values.ingress.tls | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
21
deploy/helm/pulse/templates/pvc.yaml
Normal file
21
deploy/helm/pulse/templates/pvc.yaml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "pulse.fullname" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
{{- with .Values.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- with .Values.persistence.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
14
deploy/helm/pulse/templates/server-secret.yaml
Normal file
14
deploy/helm/pulse/templates/server-secret.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{{- $secret := .Values.server.secretEnv }}
|
||||
{{- if and $secret.create (gt (len $secret.data) 0) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "pulse.serverSecretName" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- range $key, $value := $secret.data }}
|
||||
{{ $key }}: {{ $value | toString | b64enc }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
30
deploy/helm/pulse/templates/service.yaml
Normal file
30
deploy/helm/pulse/templates/service.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "pulse.fullname" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
{{- if and (eq .Values.service.type "LoadBalancer") .Values.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
{{- if and (eq .Values.service.type "LoadBalancer") (ne .Values.service.externalTrafficPolicy "") }}
|
||||
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "pulse.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
{{- if eq .Values.service.type "NodePort" }}
|
||||
{{- with .Values.service.nodePort }}
|
||||
nodePort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
12
deploy/helm/pulse/templates/serviceaccount.yaml
Normal file
12
deploy/helm/pulse/templates/serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "pulse.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "pulse.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
137
deploy/helm/pulse/values.yaml
Normal file
137
deploy/helm/pulse/values.yaml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Default values for the Pulse Helm chart.
|
||||
# This file can be used as-is for a minimal installation or as a reference when
|
||||
# overriding values (for example with `-f custom-values.yaml`).
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: rcourtman/pulse
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 7655
|
||||
annotations: {}
|
||||
loadBalancerIP: ""
|
||||
externalTrafficPolicy: Cluster
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: pulse.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: ""
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
size: 8Gi
|
||||
annotations: {}
|
||||
|
||||
server:
|
||||
env:
|
||||
- name: TZ
|
||||
value: UTC
|
||||
envFrom: []
|
||||
extraEnv: []
|
||||
extraEnvFrom: []
|
||||
secretEnv:
|
||||
create: false
|
||||
name: ""
|
||||
data: {}
|
||||
keys: []
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
podSecurityContext: {}
|
||||
securityContext: {}
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
resources: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
path: /
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
path: /
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
agent:
|
||||
enabled: false
|
||||
kind: DaemonSet # Supported: DaemonSet | Deployment
|
||||
replicaCount: 1
|
||||
serviceAccount:
|
||||
create: false
|
||||
name: ""
|
||||
annotations: {}
|
||||
image:
|
||||
repository: ghcr.io/rcourtman/pulse-docker-agent
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: PULSE_URL
|
||||
value: http://pulse:7655
|
||||
envFrom: []
|
||||
extraEnv: []
|
||||
extraEnvFrom: []
|
||||
secretEnv:
|
||||
create: false
|
||||
name: ""
|
||||
data: {}
|
||||
keys: []
|
||||
args: []
|
||||
resources: {}
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
podSecurityContext: {}
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
privileged: false
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
dockerSocket:
|
||||
enabled: true
|
||||
path: /var/run/docker.sock
|
||||
hostPathType: Socket
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
|
@ -52,6 +52,26 @@ docker run -d -p 7655:7655 -v pulse_data:/data rcourtman/pulse:latest
|
|||
|
||||
See [Docker Guide](DOCKER.md) for advanced options.
|
||||
|
||||
### Kubernetes (Helm)
|
||||
|
||||
Use the bundled Helm chart for Kubernetes clusters:
|
||||
|
||||
```bash
|
||||
helm registry login ghcr.io
|
||||
helm install pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--version $(curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/VERSION) \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
# Replace the VERSION lookup with a specific release tag (without "v") if you need to pin.
|
||||
|
||||
# Developing locally? Install from the checked-out chart directory instead:
|
||||
# helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
# --namespace pulse \
|
||||
# --create-namespace
|
||||
```
|
||||
|
||||
Read the full [Kubernetes deployment guide](KUBERNETES.md) for ingress, persistence, and Docker agent configuration.
|
||||
|
||||
## Updating
|
||||
|
||||
### Automatic Updates (Recommended)
|
||||
|
|
@ -177,4 +197,4 @@ sudo rm /etc/systemd/system/pulse.service
|
|||
docker stop pulse
|
||||
docker rm pulse
|
||||
docker volume rm pulse_data # Warning: deletes all data
|
||||
```
|
||||
```
|
||||
|
|
|
|||
176
docs/KUBERNETES.md
Normal file
176
docs/KUBERNETES.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Kubernetes Deployment (Helm)
|
||||
|
||||
Deploy Pulse to Kubernetes with the bundled Helm chart under `deploy/helm/pulse`. The chart provisions the Pulse hub (web UI + API) and can optionally run the Docker monitoring agent alongside it. Stable builds are published automatically to the GitHub Container Registry (GHCR) whenever a Pulse release goes out.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.24 or newer with access to a default `StorageClass`
|
||||
- Helm 3.9+
|
||||
- An ingress controller (only if you plan to expose Pulse through an Ingress)
|
||||
- (Optional) A Docker-compatible runtime on the nodes where you expect to run the Docker agent; the agent talks to `/var/run/docker.sock`
|
||||
|
||||
## Installing from GHCR (recommended)
|
||||
|
||||
1. Authenticate against GHCR (one-time step on each machine):
|
||||
|
||||
```bash
|
||||
helm registry login ghcr.io
|
||||
```
|
||||
|
||||
2. Install the chart published for the latest Pulse release (swap the inline `curl` command with a pinned version if you need to lock upgrades):
|
||||
|
||||
```bash
|
||||
helm install pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--version $(curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/VERSION) \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
The chart version tracks the Pulse release version. Check [GitHub Releases](https://github.com/rcourtman/Pulse/releases) or run `gh release list --limit 1` to find the newest tag if you prefer to specify it manually.
|
||||
|
||||
3. Port-forward the service to finish the first-time security setup:
|
||||
|
||||
```bash
|
||||
kubectl -n pulse port-forward svc/pulse 7655:7655
|
||||
```
|
||||
|
||||
4. Browse to `http://localhost:7655`, complete the security bootstrap (admin user + MFA + TLS preferences), and create an API token under **Settings → Security** for any automation or agents you plan to run.
|
||||
|
||||
The chart mounts a PersistentVolumeClaim at `/data` so database files, credentials, and configuration survive pod restarts. By default it requests 8 GiB with `ReadWriteOnce` access—adjust via `persistence.*` in `values.yaml`.
|
||||
|
||||
## Working From Source (local packaging)
|
||||
|
||||
Need to test local modifications or work offline? Install directly from the checked-out repository:
|
||||
|
||||
1. Clone the repository and switch into it.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rcourtman/Pulse.git
|
||||
cd Pulse
|
||||
```
|
||||
|
||||
2. Render or install the chart from `deploy/helm/pulse`:
|
||||
|
||||
```bash
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
3. Continue with the port-forward and initial setup steps described above.
|
||||
|
||||
## Common Configuration
|
||||
|
||||
Most day-to-day overrides are done in a custom values file:
|
||||
|
||||
```yaml
|
||||
# file: helm-values.yaml
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: pulse.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts: [pulse.example.com]
|
||||
secretName: pulse-tls
|
||||
|
||||
server:
|
||||
env:
|
||||
- name: TZ
|
||||
value: Europe/Berlin
|
||||
secretEnv:
|
||||
create: true
|
||||
data:
|
||||
API_TOKENS: docker-agent-token
|
||||
```
|
||||
|
||||
Install or upgrade with the overrides:
|
||||
|
||||
```bash
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
--create-namespace \
|
||||
-f helm-values.yaml
|
||||
```
|
||||
|
||||
The `server.secretEnv` block above pre-seeds one or more API tokens so the UI is immediately accessible and automation can authenticate. If you prefer to manage credentials separately, set `server.secretEnv.name` to reference an existing secret instead of letting the chart create one.
|
||||
|
||||
### Accessing Pulse
|
||||
|
||||
- **Port forward:** `kubectl -n pulse port-forward svc/pulse 7655:7655`
|
||||
- **Ingress:** Enable via the snippet above, or supply your own annotations for external DNS, TLS, etc.
|
||||
- **LoadBalancer:** Set `service.type: LoadBalancer` and, optionally, `service.loadBalancerIP`.
|
||||
|
||||
### Persistence Options
|
||||
|
||||
- `persistence.enabled`: Disable to use an ephemeral `emptyDir`
|
||||
- `persistence.existingClaim`: Bind to a pre-provisioned PVC
|
||||
- `persistence.storageClass`: Pin to a specific storage class
|
||||
- `persistence.size`: Resize the default PVC request
|
||||
|
||||
## Enabling the Docker Agent
|
||||
|
||||
The optional agent reports Docker host metrics back to the Pulse hub. Enable it once you have a valid API token:
|
||||
|
||||
```yaml
|
||||
# agent-values.yaml
|
||||
agent:
|
||||
enabled: true
|
||||
env:
|
||||
- name: PULSE_URL
|
||||
value: https://pulse.example.com
|
||||
secretEnv:
|
||||
create: true
|
||||
data:
|
||||
PULSE_TOKEN: docker-agent-token
|
||||
dockerSocket:
|
||||
enabled: true
|
||||
path: /var/run/docker.sock
|
||||
hostPathType: Socket
|
||||
```
|
||||
|
||||
Apply with (choose one):
|
||||
|
||||
- **Published chart:**
|
||||
|
||||
```bash
|
||||
helm upgrade pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--install \
|
||||
--version <pulse-version> \
|
||||
--namespace pulse \
|
||||
-f agent-values.yaml
|
||||
```
|
||||
|
||||
- **Local checkout:**
|
||||
|
||||
```bash
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
-f agent-values.yaml
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The agent expects a Docker-compatible runtime and access to the Docker socket. Set `agent.dockerSocket.enabled: false` if you run the agent elsewhere or publish the Docker API securely.
|
||||
- Use separate API tokens per host; list multiple tokens with `;` or `,` separators in `PULSE_TARGETS` if needed.
|
||||
- Run the agent as a `DaemonSet` (default) to cover every node, or switch to `agent.kind: Deployment` for a single pod.
|
||||
|
||||
## Upgrades and Removal
|
||||
|
||||
- **Upgrade (GHCR):** `helm upgrade pulse oci://ghcr.io/rcourtman/pulse-chart --version <new-version> -n pulse -f <values.yaml>`
|
||||
- **Upgrade (source):** Re-run `helm upgrade --install pulse ./deploy/helm/pulse -f <values>` with updated overrides.
|
||||
- **Rollback:** `helm rollback pulse <revision>`
|
||||
- **Uninstall:** `helm uninstall pulse -n pulse` (PVCs remain unless you delete them manually)
|
||||
|
||||
## Reference
|
||||
|
||||
- Review every available option in `deploy/helm/pulse/values.yaml`
|
||||
- Inspect published charts without installing: `helm show values oci://ghcr.io/rcourtman/pulse-chart --version <version>`
|
||||
- Helm template rendering preview: `helm template pulse ./deploy/helm/pulse -f <values>`
|
||||
- `NOTES.txt` emitted by Helm summarizes the service endpoint and agent prerequisites after each install or upgrade
|
||||
72
docs/RELEASE.md
Normal file
72
docs/RELEASE.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Pulse Release Checklist
|
||||
|
||||
Use this checklist when preparing and publishing a new Pulse release.
|
||||
|
||||
## Pre-release
|
||||
|
||||
- [ ] Ensure `VERSION` is up to date and matches the tag you plan to cut (format `4.x.y`)
|
||||
- [ ] Confirm the Helm chart renders and installs locally:
|
||||
```bash
|
||||
helm lint deploy/helm/pulse --strict
|
||||
helm template pulse deploy/helm/pulse \
|
||||
--set persistence.enabled=false \
|
||||
--set server.secretEnv.create=true \
|
||||
--set server.secretEnv.data.API_TOKENS=dummy-token
|
||||
```
|
||||
- [ ] (Optional) Run the Kind-based integration test locally:
|
||||
```bash
|
||||
kind create cluster
|
||||
helm upgrade --install pulse ./deploy/helm/pulse \
|
||||
--namespace pulse \
|
||||
--create-namespace \
|
||||
--set persistence.enabled=false \
|
||||
--set server.secretEnv.create=true \
|
||||
--set server.secretEnv.data.API_TOKENS=dummy-token \
|
||||
--wait
|
||||
kubectl -n pulse get pods
|
||||
kind delete cluster
|
||||
```
|
||||
|
||||
## Publishing
|
||||
|
||||
1. Tag the release (`git tag v4.x.y && git push origin v4.x.y`) or draft a GitHub release.
|
||||
|
||||
2. Package the Helm chart locally so you can preview the artifact (the GitHub workflow performs the same command, but local packaging provides an explicit hand-off):
|
||||
```bash
|
||||
./scripts/package-helm-chart.sh 4.x.y
|
||||
# Optional: push to GHCR after authenticating
|
||||
# helm registry login ghcr.io
|
||||
# ./scripts/package-helm-chart.sh 4.x.y --push
|
||||
```
|
||||
The script emits `dist/pulse-4.x.y.tgz`, and `scripts/build-release.sh` copies the tarball into `release/` alongside the binary archives. Uploading can be handled manually with the `--push` flag or delegated to the automated workflow described below.
|
||||
> `scripts/build-release.sh` automatically runs the same packaging step (unless you export `SKIP_HELM_PACKAGE=1`) so release archives and chart tarballs are produced together.
|
||||
|
||||
3. If you rely on automation, monitor the **Publish Helm Chart** workflow (triggered by the release) to ensure it finishes successfully. When running entirely locally, skip this step and verify the push command completed.
|
||||
|
||||
4. (Optional) Sign `release/checksums.txt` by exporting `SIGNING_KEY_ID=<gpg-key-id>` before running `scripts/build-release.sh`, or re-run the signing step manually:
|
||||
```bash
|
||||
SIGNING_KEY_ID=<gpg-key-id> ./scripts/build-release.sh
|
||||
# or sign later
|
||||
gpg --detach-sign --armor --local-user <gpg-key-id> release/checksums.txt
|
||||
```
|
||||
Publish both `checksums.txt` and `checksums.txt.asc` so users can verify artifacts:
|
||||
```bash
|
||||
gpg --verify checksums.txt.asc checksums.txt
|
||||
```
|
||||
|
||||
5. Update the release notes to include an upgrade/install snippet pointing at GHCR, for example:
|
||||
```bash
|
||||
helm install pulse oci://ghcr.io/rcourtman/pulse-chart \
|
||||
--version 4.x.y \
|
||||
--namespace pulse \
|
||||
--create-namespace
|
||||
```
|
||||
|
||||
6. Mention any chart-breaking changes (new values, migrations) in the release notes.
|
||||
|
||||
## Post-release
|
||||
|
||||
- [ ] Verify `helm show chart oci://ghcr.io/rcourtman/pulse-chart --version 4.x.y` shows the expected metadata (version, appVersion, icon)
|
||||
- [ ] Run `helm install` against a test cluster (Kind/k3s) using the published OCI artifact
|
||||
- [ ] Announce the release with links to both the GitHub release and the Helm installation instructions (`docs/KUBERNETES.md`)
|
||||
- [ ] Verify signatures: `gpg --verify checksums.txt.asc checksums.txt`
|
||||
|
|
@ -203,9 +203,43 @@ for build_name in "${!builds[@]}"; do
|
|||
cp "$BUILD_DIR/pulse-sensor-proxy-$build_name" "$RELEASE_DIR/"
|
||||
done
|
||||
|
||||
# Generate checksums (include tarballs and standalone binaries)
|
||||
cd $RELEASE_DIR
|
||||
sha256sum *.tar.gz pulse-sensor-proxy-* > checksums.txt
|
||||
# Optionally package Helm chart
|
||||
if [ "${SKIP_HELM_PACKAGE:-0}" != "1" ]; then
|
||||
if command -v helm >/dev/null 2>&1; then
|
||||
echo "Packaging Helm chart..."
|
||||
./scripts/package-helm-chart.sh "$VERSION"
|
||||
if [ -f "dist/pulse-$VERSION.tgz" ]; then
|
||||
cp "dist/pulse-$VERSION.tgz" "$RELEASE_DIR/"
|
||||
fi
|
||||
else
|
||||
echo "Helm not found on PATH; skipping Helm chart packaging. Install Helm 3.9+ or set SKIP_HELM_PACKAGE=1 to silence this message."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Generate checksums (include tarballs, helm chart, and standalone binaries)
|
||||
cd "$RELEASE_DIR"
|
||||
shopt -s nullglob
|
||||
checksum_files=( *.tar.gz pulse-sensor-proxy-* )
|
||||
if compgen -G "pulse-*.tgz" > /dev/null; then
|
||||
checksum_files+=( pulse-*.tgz )
|
||||
fi
|
||||
if [ ${#checksum_files[@]} -eq 0 ]; then
|
||||
echo "Warning: no release artifacts found to checksum."
|
||||
else
|
||||
sha256sum "${checksum_files[@]}" > checksums.txt
|
||||
if [ -n "${SIGNING_KEY_ID:-}" ]; then
|
||||
if command -v gpg >/dev/null 2>&1; then
|
||||
echo "Signing checksums with GPG key ${SIGNING_KEY_ID}..."
|
||||
gpg --batch --yes --detach-sign --armor \
|
||||
--local-user "${SIGNING_KEY_ID}" \
|
||||
--output checksums.txt.asc \
|
||||
checksums.txt
|
||||
else
|
||||
echo "SIGNING_KEY_ID is set but gpg is not installed; skipping signature."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
shopt -u nullglob
|
||||
cd ..
|
||||
|
||||
echo
|
||||
|
|
|
|||
61
scripts/package-helm-chart.sh
Executable file
61
scripts/package-helm-chart.sh
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Package (and optionally push) the Pulse Helm chart.
|
||||
# Usage:
|
||||
# ./scripts/package-helm-chart.sh [version] [--push]
|
||||
# Environment:
|
||||
# OCI_REPO (default: ghcr.io/rcourtman/pulse-chart)
|
||||
# HELM_BIN (default: helm)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CHART_DIR="$REPO_ROOT/deploy/helm/pulse"
|
||||
DIST_DIR="$REPO_ROOT/dist"
|
||||
HELM_BIN="${HELM_BIN:-helm}"
|
||||
OCI_REPO="${OCI_REPO:-ghcr.io/rcourtman/pulse-chart}"
|
||||
|
||||
if ! command -v "$HELM_BIN" >/dev/null 2>&1; then
|
||||
echo "Error: Helm not found (expected at \$HELM_BIN=$HELM_BIN). Install Helm 3.9+ first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_DEFAULT="$(cat "$REPO_ROOT/VERSION")"
|
||||
VERSION="${1:-$VERSION_DEFAULT}"
|
||||
VERSION="${VERSION#v}" # strip leading v if provided
|
||||
|
||||
PUSH=false
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--push" ]; then
|
||||
PUSH=true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Packaging Pulse chart version $VERSION (appVersion=$VERSION)"
|
||||
|
||||
rm -rf "$DIST_DIR"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
"$HELM_BIN" lint "$CHART_DIR" --strict
|
||||
"$HELM_BIN" package "$CHART_DIR" \
|
||||
--version "$VERSION" \
|
||||
--app-version "$VERSION" \
|
||||
--destination "$DIST_DIR"
|
||||
|
||||
PACKAGE_PATH="$DIST_DIR/pulse-$VERSION.tgz"
|
||||
if [ ! -f "$PACKAGE_PATH" ]; then
|
||||
echo "Error: Expected package $PACKAGE_PATH not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$PUSH" = true ]; then
|
||||
echo "Pushing chart to oci://$OCI_REPO"
|
||||
"$HELM_BIN" push "$PACKAGE_PATH" "oci://$OCI_REPO"
|
||||
fi
|
||||
|
||||
echo "Chart packaged at $PACKAGE_PATH"
|
||||
if [ "$PUSH" = true ]; then
|
||||
echo "Chart pushed to oci://$OCI_REPO"
|
||||
else
|
||||
echo "Run with --push (after logging into GHCR) to upload the artifact."
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue