mirror of
https://gh-proxy.org/https://github.com/klinsmaya/open-connector.git
synced 2026-11-08 01:16:52 +08:00
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:
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%
|
||||
|
||||
Reference in New Issue
Block a user