fix(helm): repair chart deploy blockers and publish workflow (#401)

Post-merge review of #394 turned up several defects that break supported
install paths of the new Helm chart, plus two that turn every app
release red in CI. This PR fixes them in one pass, validated with `helm
lint`, a 24-case `helm template` matrix, and `kubeconform -strict` on
every rendered shape.

Install-blocking fixes:

- The documented image pin `1.4.0` does not exist on GHCR (release tags
are v-prefixed, for example `v1.4.0`), so following the chart's own
production example ends in ImagePullBackOff. Examples and `appVersion`
now use the real tags.
- The pre-install migration Job consumed the release ConfigMap and
Secret, which do not exist yet when hooks run on a fresh install (and
hold the previous revision's values on upgrades). The Job now reads the
database URL from `database.existingUrlSecret` or a hook-scoped Secret,
and inherits the Deployment's `imagePullSecrets`, `nodeSelector`,
`tolerations`, and `affinity`.
- `readOnlyRootFilesystem` broke mail-attachment staging, which writes
to `os.tmpdir()`. The pods now mount an emptyDir at _/tmp_.
- A RollingUpdate surge pod cannot mount the ReadWriteOnce data volume
from another node and SQLite must never be opened by two pods, so the
default `updateStrategy` is now `Recreate`, and `rollingUpdate` keys
only render when the type is RollingUpdate (the old combination is
rejected by the API server).
- New install-time guards fail fast instead of deploying something
broken: `database.enabled` without a URL source silently fell back to
SQLite, multi-replica setups need PostgreSQL plus an RWX volume or
`persistence.enabled=false`, and an HPA without any metric target
silently used the cluster default.

Correctness and hygiene:

- The ConfigMap rendered every empty-string value, and an empty
`OOMOL_CONNECT_ORIGIN` overrides the runtime's localhost default and
breaks OAuth redirect URLs. Empty strings are now omitted, and the
inline PostgreSQL URL moved from the ConfigMap into the chart Secret.
- Dropped the ServiceMonitor: the runtime has no Prometheus metrics
endpoint, so the scrape target could never succeed.
- The PDB honours explicit `0` values, the migration Job name stays
within 63 characters and its pod no longer matches the Service selector,
and _values.schema.json_ now validates probe tunables and `imageDigest`.

CI (_.github/workflows/publish-helm.yml_): releases in this repository
are app releases (`v1.4.0`), which can never match chart version
`0.1.0`, so the strict validation failed every future release run, and
the visibility-promotion step called a REST endpoint that does not exist
(the Packages API has no PATCH). Mismatched release tags now skip
publishing instead of failing, the promotion step is gone, and the chart
is linted and rendered on pull requests.

Docs now match the chart's real behavior: the README and NOTES no longer
claim the chart installs without an admin token, `insecureDevelopment`
and OCI installation are documented, and the NOTES explain that a
successful migration Job is cleaned up automatically.

Refs #394
This commit is contained in:
Kevin Cui
2026-08-22 04:49:48 -04:00
committed by GitHub
parent 7f66f546bb
commit e26978e7f0
18 changed files with 424 additions and 279 deletions
+86 -90
View File
@@ -1,22 +1,30 @@
name: Publish Helm chart
# Pushes the chart at deploy/helm/open-connector to GitHub Packages (ghcr.io)
# as an OCI artifact. Mirror the tag scheme from publish-docker.yml so the
# Helm chart and Docker image advance in lock-step:
# Lints the chart at deploy/helm/open-connector on pull requests, and pushes
# it to GitHub Packages (ghcr.io) as an OCI artifact:
#
# push to main -> <chart-version>-tip (latest main build)
# <chart-version>-tip.<short-sha> (immutable ref)
# published release -> <chart-version> (when chart version
# matches the release)
# <chart-version> + latest (non-prerelease only)
# push to main -> <chart-version>-tip.<full-sha> (immutable ref,
# one per commit)
# published release -> <chart-version> (only when the
# chart version
# matches the
# release tag;
# app releases
# are skipped)
# + latest (non-prerelease
# only)
#
# Pulling the published chart:
# helm pull oci://ghcr.io/$OWNER/charts/open-connector --version 0.1.0
#
# ghcr.io packages are private by default. Promote a release to public by
# editing the package's visibility in the GitHub Packages UI, or call
# gh api -X PATCH /users/{user}/packages/container/charts%2Fopen-connector -f visibility=public
# ghcr.io packages are private by default and the REST API cannot change
# package visibility, so after the first publish flip the
# `charts/open-connector` package to public once in the GitHub Packages UI.
on:
pull_request:
paths:
- "deploy/helm/open-connector/**"
- ".github/workflows/publish-helm.yml"
push:
branches: [main]
paths:
@@ -40,8 +48,42 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
lint:
name: Lint chart
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
env:
CHART_PATH: deploy/helm/open-connector
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Set up Helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v4.2.4
- name: Helm lint
run: helm lint "${CHART_PATH}" --set secret.adminToken=ci-lint-token
- name: Render smoke tests
run: |
set -euo pipefail
# Default (SQLite) shape.
helm template smoke "${CHART_PATH}" \
--set secret.adminToken=ci-lint-token >/dev/null
# PostgreSQL shape incl. migration hook resources.
helm template smoke "${CHART_PATH}" \
--set secret.adminToken=ci-lint-token \
--set database.enabled=true \
--set database.url=postgresql://user:pass@db:5432/oc >/dev/null
echo "Rendered default and PostgreSQL shapes successfully."
publish:
name: Publish
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
env:
CHART_PATH: deploy/helm/open-connector
@@ -54,10 +96,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- name: Set up Helm
id: setup-helm
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
with:
version: v4.2.4
- name: Read chart metadata
id: meta
@@ -116,32 +162,25 @@ jobs:
id: lint
run: helm lint "${{ env.CHART_PATH }}"
- name: Validate release tag matches chart version
id: validate
if: github.event_name == 'release'
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
CHART_VERSION: ${{ steps.meta.outputs.version }}
run: |
# Strip a leading 'v' from the release tag (v1.4.0 -> 1.4.0)
# so releases can be tagged with or without the conventional prefix.
stripped="${RELEASE_TAG#v}"
if [[ "${stripped}" != "${CHART_VERSION}" ]]; then
echo "::error::Chart version ${CHART_VERSION} does not match release tag ${RELEASE_TAG}"
echo "::error::Bump Chart.yaml 'version:' to ${stripped} before publishing this release."
exit 1
fi
- name: Compute package version
id: version
env:
CHART_VERSION: ${{ steps.meta.outputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
publish=true
if [[ "${{ github.event_name }}" == "release" ]]; then
# For releases, the package version is the chart version itself.
# The validation step above has already confirmed it matches
# the release tag.
# Releases in this repository are app releases (v1.4.0, ...).
# A stable chart version is only published when Chart.yaml's
# version matches the release tag (leading 'v' optional);
# otherwise the run is a no-op rather than a failure, so app
# releases don't turn red here.
stripped="${RELEASE_TAG#v}"
version="${CHART_VERSION}"
if [[ "${stripped}" != "${CHART_VERSION}" ]]; then
publish=false
echo "::notice::Release tag ${RELEASE_TAG} is not chart version ${CHART_VERSION}; skipping chart publish (app release)."
fi
else
# For main-branch pushes, use the full triggering commit SHA as
# the unique SemVer build suffix so every commit produces a
@@ -150,7 +189,10 @@ jobs:
# eliminates the (rare) collision risk.
version="${CHART_VERSION}-tip.${{ github.sha }}"
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"
{
echo "version=${version}"
echo "publish=${publish}"
} >> "$GITHUB_OUTPUT"
# Package after the package version is known, so the .tgz filename
# matches what `Push chart OCI artifact` reads below. Previously the
@@ -159,6 +201,7 @@ jobs:
# vs. the expected open-connector-0.1.0-tip.<sha>.tgz).
- name: Helm package
id: pkg
if: steps.version.outputs.publish == 'true'
run: |
helm package "${{ env.CHART_PATH }}" \
--destination .charts \
@@ -166,6 +209,7 @@ jobs:
- name: Log in to registry
id: login
if: steps.version.outputs.publish == 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ steps.oci.outputs.host }}
@@ -174,6 +218,7 @@ jobs:
- name: Push chart OCI artifact
id: push-chart
if: steps.version.outputs.publish == 'true'
run: |
# The version embedded in the .tgz filename is what helm push
# uses as the OCI tag. We deliberately do NOT pass --version:
@@ -184,7 +229,7 @@ jobs:
- name: Tag release as 'latest'
id: tag-latest
if: github.event_name == 'release' && github.event.release.prerelease == false
if: steps.version.outputs.publish == 'true' && github.event_name == 'release' && github.event.release.prerelease == false
env:
REGISTRY: ${{ steps.oci.outputs.host }}/${{ steps.oci.outputs.namespace }}
CHART_NAME: ${{ steps.oci.outputs.chart_name }}
@@ -206,59 +251,17 @@ jobs:
"${REGISTRY}/${CHART_NAME}:${VERSION}" \
"${REGISTRY}/${CHART_NAME}:latest"
- name: Promote release to public (best-effort)
id: promote-public
if: github.event_name == 'release' && github.event.release.prerelease == false
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OWNER: ${{ steps.oci.outputs.owner }}
PACKAGE_NAME: ${{ steps.oci.outputs.package_name }}
run: |
# OCI packages on the registry are private by default; flip the
# published chart package to public so `helm pull` works without
# auth. Try the user endpoint first, then the org endpoint. The
# step exits 0 as long as at least one endpoint succeeds; if both
# fail we surface the failure as a warning and mark the step as
# failed so the workflow summary can report it (visibility can
# always be changed in the GitHub Packages UI).
encoded=$(printf '%s' "${PACKAGE_NAME}" | jq -Rr @uri)
ENDPOINTS=(
"/users/${OWNER}/packages/container/${encoded}"
"/orgs/${OWNER}/packages/container/${encoded}"
)
success_count=0
for ep in "${ENDPOINTS[@]}"; do
if gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-X PATCH \
"${ep}" \
-f visibility=public >/dev/null 2>&1; then
echo "Promoted ${PACKAGE_NAME} to public via ${ep}"
success_count=$((success_count + 1))
else
echo "::warning::Failed to flip ${PACKAGE_NAME} visibility to public at ${ep}"
fi
done
echo "promote_success_count=${success_count}" >> "$GITHUB_OUTPUT"
if [[ ${success_count} -eq 0 ]]; then
echo "::warning::Public promotion failed at all attempted endpoints. Package remains private; flip it manually in the GitHub Packages UI."
exit 1
fi
- name: Workflow summary
if: always()
env:
SETUP_HELM_RESULT: ${{ steps.setup-helm.conclusion }}
META_RESULT: ${{ steps.meta.conclusion }}
VALIDATE_RESULT: ${{ steps.validate.conclusion }}
LINT_RESULT: ${{ steps.lint.conclusion }}
PACKAGE_RESULT: ${{ steps.pkg.conclusion }}
LOGIN_RESULT: ${{ steps.login.conclusion }}
PUSH_RESULT: ${{ steps.push-chart.conclusion }}
TAG_LATEST_RESULT: ${{ steps.tag-latest.conclusion }}
PROMOTE_PUBLIC_RESULT: ${{ steps.promote-public.conclusion }}
PROMOTE_PUBLIC_SUCCESS_COUNT: ${{ steps.promote-public.outputs.promote_success_count }}
PUBLISH_GATE: ${{ steps.version.outputs.publish }}
run: |
{
# Required for "published": push-chart succeeded.
@@ -266,15 +269,13 @@ jobs:
# release); on push events and prereleases it's skipped, not failed.
push="${PUSH_RESULT:-skipped}"
tag_latest="${TAG_LATEST_RESULT:-skipped}"
promote="${PROMOTE_PUBLIC_RESULT:-skipped}"
promote_ok="${PROMOTE_PUBLIC_SUCCESS_COUNT:-0}"
if [[ "${push}" == "success" ]] && [[ "${tag_latest}" != "failure" ]]; then
if [[ "${promote}" == "success" ]]; then
echo "## Helm chart fully published"
else
echo "## Helm chart published (with warnings)"
fi
if [[ "${PUBLISH_GATE}" == "false" ]]; then
echo "## Helm chart publish skipped"
echo
echo "This release tag does not match the chart version ${{ steps.meta.outputs.version }} (app release). No chart was published."
elif [[ "${push}" == "success" ]] && [[ "${tag_latest}" != "failure" ]]; then
echo "## Helm chart published"
echo
echo "- **Chart**: \`${{ steps.meta.outputs.name }}\`"
echo "- **Version**: \`${{ steps.version.outputs.version }}\`"
@@ -286,11 +287,8 @@ jobs:
echo " ${{ env.OCI_REPO }}/${{ steps.meta.outputs.name }} \\"
echo " --version ${{ steps.version.outputs.version }}"
echo '```'
if [[ "${promote}" != "success" ]]; then
echo
echo "⚠️ **Public promotion did not complete** (\`${promote}\`). The chart OCI artifact is published but the ghcr.io package may still be private. Flip its visibility in the GitHub Packages UI or re-run this job."
fi
echo
echo "Note: the ghcr.io package is private after its first publish; flip \`${{ steps.oci.outputs.package_name }}\` to public once in the GitHub Packages UI so \`helm pull\` works without auth."
else
echo "## Helm chart publish failed"
echo
@@ -300,14 +298,12 @@ jobs:
echo "| --- | --- |"
echo "| Setup Helm | \`${SETUP_HELM_RESULT:-skipped}\` |"
echo "| Read chart metadata | \`${META_RESULT:-skipped}\` |"
echo "| Validate release tag | \`${VALIDATE_RESULT:-skipped}\` |"
echo "| Lint | \`${LINT_RESULT:-skipped}\` |"
echo "| Package | \`${PACKAGE_RESULT:-skipped}\` |"
echo "| Registry login | \`${LOGIN_RESULT:-skipped}\` |"
echo "| Push chart OCI artifact | \`${PUSH_RESULT:-skipped}\` |"
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "| Tag release as 'latest' | \`${TAG_LATEST_RESULT:-skipped}\` |"
echo "| Promote release to public | \`${PROMOTE_PUBLIC_RESULT:-skipped}\` (endpoints ok: ${promote_ok:-0}/2) |"
fi
echo
echo "Inspect the failing step's logs above for the cause."
+7 -6
View File
@@ -127,12 +127,13 @@ safe account labels, and execution results needed for the run.
## Usage Paths
| Path | Best for | Includes |
| ------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Open-source self-host | Developers and teams that want full control | Local Docker or Node runtime, SQLite or PostgreSQL state, local or S3-compatible transit files, MCP, HTTP, OpenAPI, and Web Console |
| Fly.io self-host | Teams that want a hosted Docker runtime | Node Docker runtime, SQLite on a Fly volume or external PostgreSQL, TLS, health checks, MCP, HTTP, OpenAPI, and Web Console |
| Cloudflare-compatible deploy | Teams that want a lightweight hosted runtime | Workers runtime, D1 state, R2 transit files, and Static Assets for the console |
| [OOMOL](https://oomol.com/apps) | Teams that want users to authorize accounts immediately | OOMOL-provided OAuth apps, monthly included Connect credits, and hosted runtime infrastructure; the same provider and Action contracts keep a path open to later private or self-hosted deployment |
| Path | Best for | Includes |
| --------------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Open-source self-host | Developers and teams that want full control | Local Docker or Node runtime, SQLite or PostgreSQL state, local or S3-compatible transit files, MCP, HTTP, OpenAPI, and Web Console |
| [Kubernetes (Helm)](deploy/helm/open-connector/README.md) | Teams that run their own clusters | Hardened Helm chart with PVC-backed SQLite or PostgreSQL plus migration hooks, Ingress, autoscaling, and NetworkPolicy toggles |
| Fly.io self-host | Teams that want a hosted Docker runtime | Node Docker runtime, SQLite on a Fly volume or external PostgreSQL, TLS, health checks, MCP, HTTP, OpenAPI, and Web Console |
| Cloudflare-compatible deploy | Teams that want a lightweight hosted runtime | Workers runtime, D1 state, R2 transit files, and Static Assets for the console |
| [OOMOL](https://oomol.com/apps) | Teams that want users to authorize accounts immediately | OOMOL-provided OAuth apps, monthly included Connect credits, and hosted runtime infrastructure; the same provider and Action contracts keep a path open to later private or self-hosted deployment |
## Cloudflare Quick Start Video
+4 -5
View File
@@ -30,8 +30,7 @@
OWNERS
# Tests directory is for helm-unittest; keep it out of release artifacts
tests/
# Excluding everything-as-markdown prevents the chart README from being
# packaged, but that's OK: the chart README is rendered by `helm install`
# and shipping it separately keeps the package lean.
*.md
README.md
# README.md is deliberately packaged so `helm show readme` and Artifact Hub
# can display it. (Blanket `*.md` exclusion is not worth the lost docs, and
# the `*.md` + `!README.md` negation pattern trips the Helm 4 lint bug
# described above.)
+3 -2
View File
@@ -8,8 +8,9 @@ description: |
mirror Docker Compose, plus switches for production hardening.
type: application
version: 0.1.0
appVersion: "1.4.0"
icon: https://raw.githubusercontent.com/oomol-lab/open-connector/main/assets/open-connector-icon.png
# Matches the upstream image tag (releases are tagged with a leading "v").
appVersion: "v1.4.0"
icon: https://raw.githubusercontent.com/oomol-lab/open-connector/main/web/src/assets/oomol-connect-logo.png
home: https://github.com/oomol-lab/open-connector
sources:
- https://github.com/oomol-lab/open-connector
+53 -32
View File
@@ -2,7 +2,7 @@
A production-grade Helm chart for [OpenConnector](https://github.com/oomol-lab/open-connector) - the open-source connector gateway for AI agents.
The chart defaults mirror `docker compose up` so a fresh `helm install` gives you a working runtime in seconds. Flip a handful of switches in your prod values overlay to enable Ingress, autoscaling, network policy, ServiceMonitor, and PostgreSQL-backed migrations.
The chart defaults mirror `docker compose up`. The only required input is an admin token (`secret.adminToken` or `secret.existingSecret`) - the chart refuses to install without one unless you explicitly pass `insecureDevelopment=true` for loopback-only experimentation. Flip a handful of switches in your prod values overlay to enable Ingress, autoscaling, network policy, and PostgreSQL-backed migrations.
## TL;DR
@@ -24,14 +24,32 @@ curl -s -X POST http://localhost:3000/v1/actions/hackernews.get_top_stories \
-d '{"input":{}}'
```
Published chart versions can also be installed straight from GHCR (OCI),
without cloning the repository:
```bash
helm install open-connector oci://ghcr.io/oomol-lab/charts/open-connector \
--version <chart-version> \
--set secret.adminToken="$(openssl rand -hex 24)" \
--namespace open-connector --create-namespace
```
Release versions are plain chart versions (e.g. `0.1.0`); every main-branch
build is additionally published as `<chart-version>-tip.<commit-sha>`.
## Production install (PostgreSQL + Ingress + TLS + autoscaling)
Create a `values-prod.yaml`:
```yaml
replicaCount: 3
# Multi-replica requires state to live outside the pod-local volume:
# PostgreSQL for the database, S3 for transit files, persistence off.
# The chart enforces this combination at install time.
persistence:
enabled: false
image:
tag: 1.4.0 # pin a release
tag: v1.4.0 # pin a release (release image tags carry the leading "v")
ingress:
enabled: true
className: nginx
@@ -74,9 +92,10 @@ podDisruptionBudget:
networkPolicy:
enabled: true
metrics:
enabled: true
# Allow your ingress controller's namespace, or it cannot reach the pods.
ingressNamespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
config:
origin: https://connector.example.com
@@ -102,26 +121,27 @@ helm upgrade --install open-connector ./deploy/helm/open-connector \
## Configuration
| Value | Description | Default |
| ----------------------------- | ------------------------------------------- | ---------------------------------- |
| `replicaCount` | Number of Deployment replicas | `1` |
| `image.repository` | OCI image | `ghcr.io/oomol-lab/open-connector` |
| `image.tag` | Image tag | `latest` |
| `imageDigest` | Pin by digest (overrides `tag`) | `""` |
| `service.type` | `ClusterIP` / `NodePort` / `LoadBalancer` | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `ingress.enabled` | Create an Ingress | `false` |
| `persistence.enabled` | Create a PVC for `/app/data` | `true` |
| `persistence.size` | PVC size | `5Gi` |
| `database.enabled` | Use PostgreSQL instead of SQLite | `false` |
| `database.migration.enabled` | Run a `migrate` Job as a Helm hook | `true` |
| `autoscaling.enabled` | HPA | `false` |
| `networkPolicy.enabled` | Restrict ingress to in-namespace Pods | `false` |
| `metrics.enabled` | Prometheus ServiceMonitor | `false` |
| `podDisruptionBudget.enabled` | PDB | `false` |
| `secret.existingSecret` | Use a pre-existing Secret | `""` |
| `secret.encryptionKey` | 32-byte random key (sensitive) | `""` |
| `secret.adminToken` | Bearer token for /api, /docs, / (sensitive) | `""` |
| Value | Description | Default |
| ----------------------------- | ---------------------------------------------------------------- | ---------------------------------- |
| `replicaCount` | Number of Deployment replicas | `1` |
| `image.repository` | OCI image | `ghcr.io/oomol-lab/open-connector` |
| `image.tag` | Image tag | `latest` |
| `imageDigest` | Pin by digest (overrides `tag`) | `""` |
| `service.type` | `ClusterIP` / `NodePort` / `LoadBalancer` | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `ingress.enabled` | Create an Ingress | `false` |
| `persistence.enabled` | Create a PVC for `/app/data` | `true` |
| `persistence.size` | PVC size | `5Gi` |
| `database.enabled` | Use PostgreSQL instead of SQLite | `false` |
| `database.migration.enabled` | Run a `migrate` Job as a Helm hook | `true` |
| `autoscaling.enabled` | HPA | `false` |
| `networkPolicy.enabled` | Restrict ingress to in-namespace Pods | `false` |
| `podDisruptionBudget.enabled` | PDB | `false` |
| `updateStrategy.type` | `Recreate` (safe with the RWO data volume) | `Recreate` |
| `secret.existingSecret` | Use a pre-existing Secret | `""` |
| `secret.encryptionKey` | 32-byte random key (sensitive) | `""` |
| `secret.adminToken` | Bearer token for /api, /docs, / (sensitive) | `""` |
| `insecureDevelopment` | Allow install without an admin token (loopback experiments ONLY) | `false` |
For every key under `config.*` and `secret.*`, see `docs/configuration.md` in the OpenConnector repo.
@@ -131,7 +151,7 @@ For every key under `config.*` and `secret.*`, see `docs/configuration.md` in th
helm upgrade open-connector ./deploy/helm/open-connector -f values-prod.yaml
```
The chart annotates Pod templates with the SHA-256 of the rendered ConfigMap and Secret so pods roll automatically when configuration changes.
The chart annotates Pod templates with the SHA-256 of the rendered ConfigMap and Secret so pods roll automatically when chart-managed configuration changes. Rotating an externally-managed Secret (`secret.existingSecret`, `database.existingUrlSecret`) is invisible to the chart: trigger the rollout from your secret manager (e.g. [stakater/Reloader](https://github.com/stakater/Reloader) with `podAnnotations: {reloader.stakater.com/auto: "true"}`) or bump a `podAnnotations` value.
## Rollback
@@ -159,12 +179,12 @@ kubectl -n open-connector delete pvc open-connector
Even the "easy try" defaults ship hardened:
- Runs as non-root UID `10001` (the upstream image is root; the chart overrides this).
- `readOnlyRootFilesystem: true` with the data volume mounted at `/app/data`.
- `readOnlyRootFilesystem: true` with the data volume mounted at `/app/data` and an emptyDir at `/tmp` for OS-temp staging.
- All Linux capabilities dropped, `seccompProfile: RuntimeDefault`.
- `allowPrivilegeEscalation: false`.
- `automountServiceAccountToken: false`.
Before going to production, **set `secret.encryptionKey` and `secret.adminToken` to long random values** - without an admin token the runtime still starts, but `/api`, `/docs`, and the web console are unprotected.
Before going to production, **set `secret.encryptionKey` and `secret.adminToken` to long random values**. The chart refuses to install without an admin token (or `secret.existingSecret`); the only bypass is `insecureDevelopment=true`, which leaves `/api`, `/docs`, and the web console completely unauthenticated and must never be used outside the loopback.
## Architecture map
@@ -177,14 +197,15 @@ Before going to production, **set `secret.encryptionKey` and `secret.adminToken`
| `templates/service.yaml` | ClusterIP service on port 3000 |
| `templates/ingress.yaml` | Optional Ingress |
| `templates/serviceaccount.yaml` | Hardened ServiceAccount |
| `templates/configmap.yaml` | Non-secret OOMOL*CONNECT*\* env |
| `templates/secret.yaml` | Auto-generated Secret (skipped if `existingSecret` is set) |
| `templates/configmap.yaml` | Non-secret `OOMOL_CONNECT_*` env, install-time guard rails |
| `templates/secret.yaml` | Auto-generated Secret for the app secrets |
| `templates/secret-db.yaml` | Secret for an inline `database.url` |
| `templates/pvc.yaml` | Persistent volume for SQLite/transit files |
| `templates/hpa.yaml` | Optional HPA |
| `templates/poddisruptionbudget.yaml` | Optional PDB |
| `templates/networkpolicy.yaml` | Optional NetworkPolicy |
| `templates/servicemonitor.yaml` | Optional Prometheus ServiceMonitor |
| `templates/migration-job.yaml` | Helm-hook Job for `runtime:migrate` |
| `templates/migration-job.yaml` | Helm-hook Job running `open-connector migrate` |
| `templates/migration-secret.yaml` | Hook-scoped Secret feeding the migration Job |
| `templates/NOTES.txt` | Post-install instructions |
## License
+31 -17
View File
@@ -7,18 +7,26 @@ Thanks for installing {{ .Chart.Name }} {{ .Chart.Version }} (app version {{ .Ch
The release is configured with a single replica and a ClusterIP service.
kubectl --namespace {{ .Release.Namespace }} port-forward \
svc/{{ include "open-connector.fullname" . }} 3000:3000
svc/{{ include "open-connector.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
Then open:
- Web console: http://localhost:3000
- API docs: http://localhost:3000/docs
- OpenAPI: http://localhost:3000/openapi.json
- Health: http://localhost:3000/health
- Web console: http://localhost:{{ .Values.service.port }}
- API docs: http://localhost:{{ .Values.service.port }}/docs
- OpenAPI: http://localhost:{{ .Values.service.port }}/openapi.json
- Health: http://localhost:{{ .Values.service.port }}/health
No admin token is set yet. Set `secret.adminToken` (or wire an existing
Secret) before exposing the admin API, docs, or web console outside the
loopback.
{{- if and (not .Values.secret.adminToken) (not .Values.secret.existingSecret) }}
WARNING: insecureDevelopment is enabled and no admin token is set. The
admin API, docs, and web console are completely unauthenticated. Set
`secret.adminToken` (or wire an existing Secret) before exposing this
release outside the loopback.
{{- else }}
The admin surfaces (/api, /docs, web console) require the configured
admin token.
{{- end }}
==========================================================================
First-call smoke test
@@ -27,10 +35,12 @@ loopback.
{{- if .Values.database.enabled }}
This release uses PostgreSQL. The migration Job ran as a Helm hook
(pre-install/pre-upgrade); verify it succeeded with:
(pre-install/pre-upgrade). A successful Job is cleaned up automatically,
so an empty list here is expected on success - only failed Jobs stick
around for debugging:
kubectl --namespace {{ .Release.Namespace }} get jobs \
-l "app.kubernetes.io/name={{ include "open-connector.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
-l "app.kubernetes.io/component=migrate,app.kubernetes.io/instance={{ .Release.Name }}"
{{- else }}
@@ -41,7 +51,7 @@ database automatically on first start.
Smoke-test a no-auth Action:
curl -s -X POST http://localhost:3000/v1/actions/hackernews.get_top_stories \
curl -s -X POST http://localhost:{{ .Values.service.port }}/v1/actions/hackernews.get_top_stories \
-H 'content-type: application/json' \
-d '{"input":{}}'
@@ -53,11 +63,15 @@ Before exposing this runtime to the public:
[ ] Set `secret.encryptionKey` to a long random value.
[ ] Set `secret.adminToken` and gate /api, /docs, / via Ingress + auth.
[ ] Set `image.tag` (or `imageDigest`) to a pinned version, not "latest".
[ ] Enable `ingress`, `networkPolicy`, `podDisruptionBudget`, and `metrics`.
[ ] Raise `replicaCount`, enable `autoscaling`, switch to PostgreSQL
(`database.enabled=true`), and configure S3 transit
(`config.transitFileBackend=s3`).
[ ] Set `image.tag` (e.g. "v1.4.0") or `imageDigest` to a pinned
version, not "latest".
[ ] Enable `ingress` and `networkPolicy` (with an
`ingressNamespaceSelector` for your ingress controller).
[ ] Switch to PostgreSQL (`database.enabled=true`) and S3 transit files
(`config.transitFileBackend=s3`), then set `persistence.enabled=false`
before raising `replicaCount` / enabling `autoscaling` - the default
ReadWriteOnce data volume cannot be shared between replicas.
[ ] Enable `podDisruptionBudget` once you run 2+ replicas.
[ ] Move `secret.*` out of values.yaml and into a Secret manager
(`secret.existingSecret`) or External Secrets Operator.
[ ] Set `config.origin` to your public https origin (used for OAuth redirects).
@@ -65,4 +79,4 @@ Before exposing this runtime to the public:
your agents need.
For more, see:
https://github.com/oomol-lab/open-connector/blob/main/docs/configuration.md
https://github.com/oomol-lab/open-connector/blob/main/docs/configuration.md
@@ -43,9 +43,6 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/part-of: open-connector
{{- with .Values.podLabels }}
{{ toYaml . }}
{{- end }}
{{- end -}}
{{/*
@@ -117,7 +114,7 @@ Map a snake-case secret key name to its OOMOL_CONNECT_* env-var name.
{{- else if eq . "s3AccessKeyId" -}}OOMOL_CONNECT_S3_ACCESS_KEY_ID
{{- else if eq . "s3SecretAccessKey" -}}OOMOL_CONNECT_S3_SECRET_ACCESS_KEY
{{- else if eq . "s3SessionToken" -}}OOMOL_CONNECT_S3_SESSION_TOKEN
{{- else -}}OOMOL_CONNECT_{{ . | upper }}
{{- else -}}OOMOL_CONNECT_{{ . | snakecase | upper }}
{{- end -}}
{{- end -}}
@@ -143,6 +140,6 @@ Map a config key name to its OOMOL_CONNECT_* env-var name.
{{- else if eq . "runLimit" -}}OOMOL_CONNECT_RUN_LIMIT
{{- else if eq . "dataDir" -}}OOMOL_CONNECT_DATA_DIR
{{- else if eq . "origin" -}}OOMOL_CONNECT_ORIGIN
{{- else -}}OOMOL_CONNECT_{{ . | upper }}
{{- else -}}OOMOL_CONNECT_{{ . | snakecase | upper }}
{{- end -}}
{{- end -}}
@@ -17,6 +17,29 @@
{{- fail "open-connector: when secret.existingSecret is set, secret.secretKeys must include 'adminToken'. Without it the admin surface is unauthenticated." }}
{{- end }}
{{- end }}
{{- if .Values.database.enabled }}
{{- if and .Values.database.url .Values.database.existingUrlSecret }}
{{- fail "open-connector: set either database.url or database.existingUrlSecret, not both." }}
{{- end }}
{{- if not (or .Values.database.url .Values.database.existingUrlSecret) }}
{{- fail "open-connector: database.enabled=true requires database.url or database.existingUrlSecret. Without a URL the runtime silently falls back to SQLite." }}
{{- end }}
{{- end }}
{{- /*
The data volume is shared by every replica. A single SQLite file must
never be opened by more than one pod, and a ReadWriteOnce PVC cannot be
mounted across nodes, so multi-replica setups must move their state out
of the volume entirely.
*/ -}}
{{- $maxReplicas := int (ternary .Values.autoscaling.maxReplicas .Values.replicaCount .Values.autoscaling.enabled) }}
{{- if and .Values.persistence.enabled (gt $maxReplicas 1) }}
{{- if not .Values.database.enabled }}
{{- fail "open-connector: more than one replica (replicaCount or autoscaling.maxReplicas) requires PostgreSQL (database.enabled=true) - the default SQLite database cannot be shared between pods." }}
{{- end }}
{{- if not (has "ReadWriteMany" .Values.persistence.accessModes) }}
{{- fail "open-connector: more than one replica with persistence.enabled=true requires a ReadWriteMany PVC. Either set persistence.enabled=false (with database.enabled=true and config.transitFileBackend=s3) or use an RWX storage class in persistence.accessModes." }}
{{- end }}
{{- end }}
apiVersion: v1
kind: ConfigMap
@@ -25,21 +48,26 @@ metadata:
labels:
{{- include "open-connector.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.config }}
{{- /* Merge the dataDir default in before rendering so the env loop stays uniform. */}}
{{- $config := merge (dict) .Values.config }}
{{- if not $config.dataDir }}
{{- $_ := set $config "dataDir" .Values.persistence.mountPath }}
{{- end }}
{{- range $key, $value := $config }}
{{- $envName := include "open-connector.configEnvName" $key }}
{{- /* Skip empty strings: an empty env var would override the runtime's
built-in default (e.g. OOMOL_CONNECT_ORIGIN="" breaks OAuth
redirect URLs), while booleans and numbers always render. */}}
{{- if or (eq (printf "%T" $value) "float64") (eq (printf "%T" $value) "int") }}
{{ $envName }}: {{ $value | int | quote }}
{{- else }}
{{- else if or $value (eq (printf "%T" $value) "bool") }}
{{ $envName }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- if .Values.database.enabled }}
{{- if not .Values.database.existingUrlSecret }}
OOMOL_CONNECT_DATABASE_URL: {{ .Values.database.url | quote }}
{{- end }}
OOMOL_CONNECT_DATABASE_POOL_MAX: {{ .Values.database.poolMax | quote }}
OOMOL_CONNECT_DATABASE_CONNECT_TIMEOUT_MS: {{ .Values.database.connectTimeoutMs | quote }}
{{- end }}
NODE_ENV: production
PORT: "3000"
HOST: 0.0.0.0
HOST: 0.0.0.0
@@ -9,7 +9,11 @@ spec:
replicas: {{ .Values.replicaCount }}
{{- end }}
strategy:
{{- toYaml .Values.updateStrategy | nindent 4 }}
type: {{ .Values.updateStrategy.type }}
{{- if and (eq .Values.updateStrategy.type "RollingUpdate") .Values.updateStrategy.rollingUpdate }}
rollingUpdate:
{{- toYaml .Values.updateStrategy.rollingUpdate | nindent 6 }}
{{- end }}
selector:
matchLabels:
{{- include "open-connector.selectorLabels" . | nindent 6 }}
@@ -18,15 +22,16 @@ spec:
labels:
{{- include "open-connector.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{ toYaml . | nindent 8 }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
# Roll the pod when the Secret or ConfigMap changes; secrets and
# configmaps aren't auto-detected by the kubelet.
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
checksum/db-secret: {{ include (print $.Template.BasePath "/secret-db.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{ toYaml . | nindent 8 }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "open-connector.serviceAccountName" . }}
@@ -83,6 +88,13 @@ spec:
name: {{ .Values.database.existingUrlSecret }}
key: {{ .Values.database.existingUrlSecretKey | default "url" }}
optional: false
{{- else if and .Values.database.enabled .Values.database.url }}
- name: OOMOL_CONNECT_DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ printf "%s-db" (include "open-connector.fullname" . | trunc 60 | trimSuffix "-") }}
key: databaseUrl
optional: false
{{- end }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
@@ -122,10 +134,16 @@ spec:
volumeMounts:
- name: data
mountPath: {{ .Values.persistence.mountPath }}
# The root filesystem is read-only; mail-attachment staging
# writes to the OS temp dir.
- name: tmp
mountPath: /tmp
{{- with .Values.extraVolumeMounts }}
{{ toYaml . | nindent 12 }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: tmp
emptyDir: {}
- name: data
{{- if and .Values.persistence.enabled .Values.persistence.existingClaim }}
persistentVolumeClaim:
@@ -137,7 +155,7 @@ spec:
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{ toYaml . | nindent 8 }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
@@ -155,7 +173,7 @@ spec:
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.podAntiAffinity.preferredDuringScheduling }}
{{- if .Values.topologySpread.enabled }}
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
@@ -1,4 +1,7 @@
{{- if .Values.autoscaling.enabled -}}
{{- if not (or .Values.autoscaling.targetCPUUtilizationPercentage .Values.autoscaling.targetMemoryUtilizationPercentage) }}
{{- fail "open-connector: autoscaling.enabled=true requires targetCPUUtilizationPercentage or targetMemoryUtilizationPercentage (an HPA without metrics silently applies the cluster default)." }}
{{- end }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
@@ -1,10 +1,14 @@
{{- if and .Values.database.enabled .Values.database.migration.enabled -}}
{{- /* Truncate the base name so the appended suffix stays inside the
63-character name limit. */ -}}
{{- $jobName := printf "%s-migrate" (include "open-connector.fullname" . | trunc 55 | trimSuffix "-") }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "open-connector.fullname" . }}-migrate
name: {{ $jobName }}
labels:
{{- include "open-connector.labels" . | nindent 4 }}
app.kubernetes.io/component: migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
@@ -13,9 +17,14 @@ spec:
backoffLimit: {{ .Values.database.migration.backoffLimit }}
template:
metadata:
name: {{ include "open-connector.fullname" . }}-migrate
name: {{ $jobName }}
labels:
{{- include "open-connector.selectorLabels" . | nindent 8 }}
{{- /* Deliberately NOT the Deployment's selector labels: the hook
pod must not be picked up by the Service, PDB, or
NetworkPolicy selectors. */}}
app.kubernetes.io/name: {{ include "open-connector.name" . }}-migrate
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: migrate
spec:
restartPolicy: OnFailure
securityContext:
@@ -27,37 +36,50 @@ spec:
image: {{ include "open-connector.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["/usr/local/bin/open-connector", "migrate"]
envFrom:
- configMapRef:
name: {{ include "open-connector.fullname" . }}
{{- /* The migrate entrypoint only reads the database settings.
It must not consume the release ConfigMap or Secret: as a
pre-install hook it runs before regular resources exist, and
on upgrades it would read the previous revision's values.
The URL comes from the external Secret or from the
hook-scoped copy in migration-secret.yaml. */}}
env:
{{- if .Values.secret.existingSecret }}
{{- range $key := .Values.secret.secretKeys }}
- name: {{ include "open-connector.secretEnvName" $key }}
valueFrom:
secretKeyRef:
name: {{ $.Values.secret.existingSecret }}
key: {{ $key }}
optional: true
{{- end }}
{{- else }}
{{- range $key := .Values.secret.secretKeys }}
- name: {{ include "open-connector.secretEnvName" $key }}
valueFrom:
secretKeyRef:
name: {{ include "open-connector.secretName" $ }}
key: {{ $key }}
optional: true
{{- end }}
{{- end }}
{{- if and .Values.database.enabled .Values.database.existingUrlSecret }}
- name: OOMOL_CONNECT_DATABASE_URL
valueFrom:
secretKeyRef:
{{- if .Values.database.existingUrlSecret }}
name: {{ .Values.database.existingUrlSecret }}
key: {{ .Values.database.existingUrlSecretKey | default "url" }}
{{- else }}
name: {{ $jobName }}
key: databaseUrl
{{- end }}
optional: false
{{- end }}
- name: OOMOL_CONNECT_DATABASE_POOL_MAX
value: {{ .Values.database.poolMax | quote }}
- name: OOMOL_CONNECT_DATABASE_CONNECT_TIMEOUT_MS
value: {{ .Values.database.connectTimeoutMs | quote }}
volumeMounts:
- name: tmp
mountPath: /tmp
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
volumes:
- name: tmp
emptyDir: {}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
@@ -0,0 +1,26 @@
{{- /* Hook-scoped copy of the database URL for the migration Job. The
regular chart Secret does not exist yet when pre-install hooks run,
and on upgrades it still holds the previous revision's values, so the
Job gets its own Secret created in the same hook phase (with a lower
weight so it exists before the Job starts). hook-succeeded is safe
despite the lower weight: Helm deletes hook-succeeded resources only
after every hook of the event has completed, so the Secret outlives
the Job run and is cleaned up with it on success. After a failed
migration both linger for debugging (and are replaced on the next run
via before-hook-creation). */ -}}
{{- if and .Values.database.enabled .Values.database.migration.enabled .Values.database.url (not .Values.database.existingUrlSecret) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ printf "%s-migrate" (include "open-connector.fullname" . | trunc 55 | trimSuffix "-") }}
labels:
{{- include "open-connector.labels" . | nindent 4 }}
app.kubernetes.io/component: migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
type: Opaque
stringData:
databaseUrl: {{ .Values.database.url | quote }}
{{- end }}
@@ -1,4 +1,8 @@
{{- if .Values.podDisruptionBudget.enabled -}}
{{- /* Explicit 0 is a valid setting and must not fall through to the
default, so test for "value present" rather than truthiness. */ -}}
{{- $minAvailable := toString .Values.podDisruptionBudget.minAvailable }}
{{- $maxUnavailable := toString .Values.podDisruptionBudget.maxUnavailable }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
@@ -6,9 +10,9 @@ metadata:
labels:
{{- include "open-connector.labels" . | nindent 4 }}
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
{{- if ne $minAvailable "" }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- else if .Values.podDisruptionBudget.maxUnavailable }}
{{- else if ne $maxUnavailable "" }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- else }}
minAvailable: 1
@@ -16,4 +20,4 @@ spec:
selector:
matchLabels:
{{- include "open-connector.selectorLabels" . | nindent 6 }}
{{- end }}
{{- end }}
@@ -0,0 +1,16 @@
{{- /* Dedicated Secret for an inline database URL, so credentials never
land in the ConfigMap and the name cannot collide with a
`secret.existingSecret` that happens to match the release fullname.
Rendered independently of the app Secret because an external app
Secret can be combined with an inline database URL. */ -}}
{{- if and .Values.database.enabled .Values.database.url (not .Values.database.existingUrlSecret) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ printf "%s-db" (include "open-connector.fullname" . | trunc 60 | trimSuffix "-") }}
labels:
{{- include "open-connector.labels" . | nindent 4 }}
type: Opaque
stringData:
databaseUrl: {{ .Values.database.url | quote }}
{{- end }}
@@ -17,4 +17,4 @@ stringData:
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,24 +0,0 @@
{{- if .Values.metrics.enabled -}}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "open-connector.fullname" . }}
labels:
{{- include "open-connector.labels" . | nindent 4 }}
{{- with .Values.metrics.serviceMonitor.labels }}
{{ toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.metrics.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "open-connector.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: /health
interval: {{ .Values.metrics.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }}
{{- end }}
+28 -34
View File
@@ -10,14 +10,14 @@
},
"image": {
"type": "object",
"required": ["repository", "tag"],
"required": ["repository"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"imageDigest": { "type": "string" },
"imageDigest": { "type": "string", "pattern": "^(sha256:[0-9a-f]{64})?$" },
"imagePullSecrets": {
"type": "array",
"items": {
@@ -39,10 +39,10 @@
},
"podSecurityContext": { "type": "object" },
"securityContext": { "type": "object" },
"podAntiAffinity": {
"topologySpread": {
"type": "object",
"properties": {
"preferredDuringScheduling": { "type": "boolean" }
"enabled": { "type": "boolean" }
}
},
"service": {
@@ -85,7 +85,6 @@
"type": "array",
"items": {
"type": "object",
"required": ["hosts"],
"properties": {
"secretName": { "type": "string" },
"hosts": { "type": "array", "items": { "type": "string" } }
@@ -98,18 +97,9 @@
"probes": {
"type": "object",
"properties": {
"liveness": {
"type": "object",
"properties": { "enabled": { "type": "boolean" }, "path": { "type": "string" } }
},
"readiness": {
"type": "object",
"properties": { "enabled": { "type": "boolean" }, "path": { "type": "string" } }
},
"startup": {
"type": "object",
"properties": { "enabled": { "type": "boolean" }, "path": { "type": "string" } }
}
"liveness": { "$ref": "#/$defs/probe" },
"readiness": { "$ref": "#/$defs/probe" },
"startup": { "$ref": "#/$defs/probe" }
}
},
"persistence": {
@@ -219,30 +209,34 @@
"additionalIngressNamespaceSelector": { "type": "object" }
}
},
"metrics": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"serviceMonitor": {
"type": "object",
"properties": {
"interval": { "type": "string" },
"scrapeTimeout": { "type": "string" },
"annotations": { "type": "object" },
"labels": { "type": "object" }
}
}
}
},
"nodeSelector": { "type": "object" },
"tolerations": { "type": "array" },
"affinity": { "type": "object" },
"podAnnotations": { "type": "object" },
"podLabels": { "type": "object" },
"updateStrategy": { "type": "object" },
"updateStrategy": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["RollingUpdate", "Recreate"] },
"rollingUpdate": { "type": "object" }
}
},
"extraEnv": { "type": "array" },
"extraVolumes": { "type": "array" },
"extraVolumeMounts": { "type": "array" },
"terminationGracePeriodSeconds": { "type": "integer", "minimum": 0 }
},
"$defs": {
"probe": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"path": { "type": "string" },
"initialDelaySeconds": { "type": "integer", "minimum": 0 },
"periodSeconds": { "type": "integer", "minimum": 1 },
"timeoutSeconds": { "type": "integer", "minimum": 1 },
"failureThreshold": { "type": "integer", "minimum": 1 }
}
}
}
}
+49 -20
View File
@@ -1,9 +1,11 @@
# ============================================================================
# OpenConnector Helm chart - values.yaml
# ----------------------------------------------------------------------------
# Defaults are tuned so that `helm install open-connector .` gives you a
# working single-replica deployment on `kubectl port-forward`, mirroring
# `docker compose up`. Flip the toggles at the bottom for production use.
# Defaults are tuned for a single-replica deployment on
# `kubectl port-forward`, mirroring `docker compose up`. The chart refuses
# to install until you set `secret.adminToken` or `secret.existingSecret`
# (or explicitly opt in to `insecureDevelopment: true` for local
# experimentation). Flip the toggles at the bottom for production use.
# ============================================================================
# Number of Deployment replicas (ignored when autoscaling is enabled).
@@ -14,8 +16,10 @@ image:
# Tag is the upstream image tag. Common choices:
# "latest" - latest released version (use with caution in production)
# "tip" - latest main-branch build
# "1.4.0" - pin to a specific released version (recommended)
# "v1.4.0" - pin to a specific released version (recommended; release
# tags carry the leading "v")
# "<sha>" - pin to a specific commit
# Leave empty to fall back to the chart's appVersion.
tag: latest
pullPolicy: IfNotPresent
@@ -56,9 +60,9 @@ securityContext:
drop:
- ALL
# Optional pod-level anti-affinity.
podAntiAffinity:
preferredDuringScheduling: true
# Spread replicas across nodes (soft preference via topologySpreadConstraints).
topologySpread:
enabled: true
# ClusterIP matches docker-compose's port-published container. Use NodePort
# or LoadBalancer if you don't front it with an Ingress.
@@ -111,8 +115,16 @@ probes:
timeoutSeconds: 3
failureThreshold: 30
# Mirrors the `connector-data` named volume in docker-compose. SQLite,
# transit files, and tmp staging all live under /app/data.
# Mirrors the `connector-data` named volume in docker-compose. SQLite and
# transit-file staging live under /app/data. (Mail-attachment staging uses
# the OS temp dir; the chart mounts an emptyDir at /tmp for that, since the
# root filesystem is read-only.)
#
# NOTE: the PVC is ReadWriteOnce by default and cannot be shared across
# replicas on different nodes. To scale beyond one replica, switch to
# PostgreSQL (database.enabled=true) plus S3 transit files
# (config.transitFileBackend=s3) and set persistence.enabled=false - the
# chart enforces this at install time.
persistence:
enabled: true
mountPath: /app/data
@@ -126,9 +138,13 @@ persistence:
# Non-secret runtime configuration. Each key is exposed as the matching
# OOMOL_CONNECT_<UPPER_SNAKE> environment variable. See
# https://github.com/oomol-lab/open-connector/blob/main/docs/configuration.md
# Keys left as empty strings are omitted from the ConfigMap entirely so the
# runtime's built-in defaults apply (an empty-string env var would override
# them).
config:
origin: ""
dataDir: /app/data
# Defaults to persistence.mountPath when left empty.
dataDir: ""
allowedActions: ""
blockedActions: ""
allowedProxies: ""
@@ -162,6 +178,13 @@ secret:
# Reference an externally-managed Secret (sealed-secrets, ESO, Vault).
# When set, the auto-generated Secret is NOT created and the keys below
# are ignored. The Secret must contain every key listed in `secretKeys`.
#
# NOTE: rotating an externally-managed Secret does not restart pods (the
# chart can only checksum the Secret it renders itself). Trigger a rollout
# from your secret manager, e.g. run stakater/Reloader and set
# `podAnnotations: {reloader.stakater.com/auto: "true"}`, or bump any
# podAnnotations value after a rotation. The same applies to
# `database.existingUrlSecret`.
existingSecret: ""
secretKeys:
- encryptionKey
@@ -185,7 +208,9 @@ secret:
s3SessionToken: ""
# SQLite by default (matches docker-compose). Set enabled=true and provide
# a URL to switch to PostgreSQL - also enables the migration Job.
# a URL (either `url` or `existingUrlSecret`, not both) to switch to
# PostgreSQL - also enables the migration Job. When `url` is used it is
# stored in the chart-managed Secret, not the ConfigMap.
database:
enabled: false
url: ""
@@ -204,24 +229,22 @@ autoscaling:
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: ""
# NOTE: enabling this with a single replica blocks voluntary node drains
# (minAvailable 1 of 1). Only enable it once you run 2+ replicas.
podDisruptionBudget:
enabled: false
minAvailable: ""
maxUnavailable: ""
# When enabled together with an Ingress, add a namespace selector matching
# your ingress controller's namespace (e.g.
# ingressNamespaceSelector: {matchLabels: {kubernetes.io/metadata.name: ingress-nginx}})
# or the controller cannot reach the pods.
networkPolicy:
enabled: false
ingressNamespaceSelector: {}
additionalIngressNamespaceSelector: {}
metrics:
enabled: false
serviceMonitor:
interval: 30s
scrapeTimeout: 10s
annotations: {}
labels: {}
nodeSelector: {}
tolerations: []
affinity: {}
@@ -229,8 +252,14 @@ affinity: {}
podAnnotations: {}
podLabels: {}
# Recreate by default: the data volume is ReadWriteOnce, so a RollingUpdate
# surge pod scheduled on another node can never mount it and the rollout
# deadlocks (and SQLite must not be opened by two pods at once). Switch to
# RollingUpdate only after moving to PostgreSQL + S3 transit files with
# persistence disabled. The rollingUpdate block is ignored unless
# type is RollingUpdate.
updateStrategy:
type: RollingUpdate
type: Recreate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%