diff --git a/.github/workflows/preview-helm-charts.yml b/.github/workflows/preview-helm-charts.yml index c8c7aa98..798fb24f 100644 --- a/.github/workflows/preview-helm-charts.yml +++ b/.github/workflows/preview-helm-charts.yml @@ -19,6 +19,7 @@ jobs: image-loader: ${{ steps.changes.outputs.image-loader }} automation: ${{ steps.changes.outputs.automation }} plugin-directory: ${{ steps.changes.outputs.plugin-directory }} + integrations-hub: ${{ steps.changes.outputs.integrations-hub }} openhands: ${{ steps.changes.outputs.openhands }} openhands-secrets: ${{ steps.changes.outputs.openhands-secrets }} steps: @@ -39,7 +40,7 @@ jobs: echo "$CHANGED_FILES" # Check each chart for changes - for chart in crd-check runtime-api image-loader automation plugin-directory openhands openhands-secrets; do + for chart in crd-check runtime-api image-loader automation plugin-directory integrations-hub openhands openhands-secrets; do if echo "$CHANGED_FILES" | grep -q "^charts/${chart}/"; then echo "${chart}=true" >> $GITHUB_OUTPUT echo "Changes detected in charts/${chart}" @@ -62,13 +63,15 @@ jobs: matrix: chart: # Order matters! Charts that are dependencies must be published first. - # crd-check, automation, plugin-directory, and runtime-api are dependencies of openhands. + # crd-check, automation, plugin-directory, integrations-hub, and runtime-api are dependencies of openhands. - name: crd-check path: charts/crd-check - name: automation path: charts/automation - name: plugin-directory path: charts/plugin-directory + - name: integrations-hub + path: charts/integrations-hub - name: runtime-api path: charts/runtime-api - name: image-loader @@ -88,6 +91,7 @@ jobs: HAS_CHANGES_IMAGE_LOADER: ${{ needs.detect-changes.outputs.image-loader }} HAS_CHANGES_AUTOMATION: ${{ needs.detect-changes.outputs.automation }} HAS_CHANGES_PLUGIN_DIRECTORY: ${{ needs.detect-changes.outputs.plugin-directory }} + HAS_CHANGES_INTEGRATIONS_HUB: ${{ needs.detect-changes.outputs.integrations-hub }} HAS_CHANGES_OPENHANDS: ${{ needs.detect-changes.outputs.openhands }} HAS_CHANGES_OPENHANDS_SECRETS: ${{ needs.detect-changes.outputs.openhands-secrets }} IS_PUBLISHABLE_CRD_CHECK: ${{ needs.validate-chart-versions.outputs.crd-check-publishable }} @@ -95,6 +99,7 @@ jobs: IS_PUBLISHABLE_IMAGE_LOADER: ${{ needs.validate-chart-versions.outputs.image-loader-publishable }} IS_PUBLISHABLE_AUTOMATION: ${{ needs.validate-chart-versions.outputs.automation-publishable }} IS_PUBLISHABLE_PLUGIN_DIRECTORY: ${{ needs.validate-chart-versions.outputs.plugin-directory-publishable }} + IS_PUBLISHABLE_INTEGRATIONS_HUB: ${{ needs.validate-chart-versions.outputs.integrations-hub-publishable }} IS_PUBLISHABLE_OPENHANDS: ${{ needs.validate-chart-versions.outputs.openhands-publishable }} IS_PUBLISHABLE_OPENHANDS_SECRETS: ${{ needs.validate-chart-versions.outputs.openhands-secrets-publishable }} run: | @@ -120,6 +125,10 @@ jobs: HAS_CHANGES="$HAS_CHANGES_PLUGIN_DIRECTORY" IS_PUBLISHABLE="$IS_PUBLISHABLE_PLUGIN_DIRECTORY" ;; + integrations-hub) + HAS_CHANGES="$HAS_CHANGES_INTEGRATIONS_HUB" + IS_PUBLISHABLE="$IS_PUBLISHABLE_INTEGRATIONS_HUB" + ;; openhands) HAS_CHANGES="$HAS_CHANGES_OPENHANDS" IS_PUBLISHABLE="$IS_PUBLISHABLE_OPENHANDS" @@ -203,6 +212,14 @@ jobs: yq -i "(.dependencies[] | select(.name == \"plugin-directory\")).version = \"${PLUGIN_DIRECTORY_PREVIEW}\"" charts/openhands/Chart.yaml + - name: Update integrations-hub dependency version + if: steps.check.outputs.should_publish == 'true' && matrix.chart.name == 'openhands' && needs.detect-changes.outputs.integrations-hub == 'true' + run: | + INTEGRATIONS_HUB_VERSION=$(yq '.version' charts/integrations-hub/Chart.yaml) + INTEGRATIONS_HUB_PREVIEW="${INTEGRATIONS_HUB_VERSION}-alpha.${{ github.event.pull_request.number }}" + + yq -i "(.dependencies[] | select(.name == \"integrations-hub\")).version = \"${INTEGRATIONS_HUB_PREVIEW}\"" charts/openhands/Chart.yaml + - name: Test ${{ matrix.chart.name }} chart with default values if: steps.check.outputs.should_publish == 'true' run: | @@ -279,6 +296,8 @@ jobs: path: charts/automation - name: plugin-directory path: charts/plugin-directory + - name: integrations-hub + path: charts/integrations-hub - name: openhands path: charts/openhands - name: openhands-secrets @@ -331,6 +350,15 @@ jobs: echo "Updating openhands plugin-directory dependency to ${PLUGIN_DIRECTORY_PREVIEW}" yq -i "(.dependencies[] | select(.name == \"plugin-directory\")).version = \"${PLUGIN_DIRECTORY_PREVIEW}\"" charts/openhands/Chart.yaml + - name: Update integrations-hub dependency version for openhands + if: matrix.chart.name == 'openhands' && needs.detect-changes.outputs.integrations-hub == 'true' + run: | + INTEGRATIONS_HUB_VERSION=$(yq '.version' charts/integrations-hub/Chart.yaml) + INTEGRATIONS_HUB_PREVIEW="${INTEGRATIONS_HUB_VERSION}-alpha.${{ github.event.pull_request.number }}" + + echo "Updating openhands integrations-hub dependency to ${INTEGRATIONS_HUB_PREVIEW}" + yq -i "(.dependencies[] | select(.name == \"integrations-hub\")).version = \"${INTEGRATIONS_HUB_PREVIEW}\"" charts/openhands/Chart.yaml + - name: Lint ${{ matrix.chart.name }} chart run: | echo "Testing ${{ matrix.chart.name }} chart" diff --git a/.github/workflows/publish-helm-charts.yml b/.github/workflows/publish-helm-charts.yml index ef7686e9..9b440582 100644 --- a/.github/workflows/publish-helm-charts.yml +++ b/.github/workflows/publish-helm-charts.yml @@ -28,7 +28,7 @@ jobs: matrix: chart: # Order matters: openhands depends on crd-check, runtime-api, automation, - # and plugin-directory, so those must publish first. + # plugin-directory, and integrations-hub, so those must publish first. - name: crd-check path: charts/crd-check - name: runtime-api @@ -39,6 +39,8 @@ jobs: path: charts/automation - name: plugin-directory path: charts/plugin-directory + - name: integrations-hub + path: charts/integrations-hub - name: openhands path: charts/openhands - name: openhands-secrets diff --git a/.github/workflows/validate-chart-versions.yml b/.github/workflows/validate-chart-versions.yml index 64003041..bf57a9e2 100644 --- a/.github/workflows/validate-chart-versions.yml +++ b/.github/workflows/validate-chart-versions.yml @@ -29,6 +29,9 @@ on: plugin-directory-publishable: description: 'Whether plugin-directory chart is publishable (no changes or version bumped)' value: ${{ jobs.validate-chart-versions.outputs.plugin-directory-publishable }} + integrations-hub-publishable: + description: 'Whether integrations-hub chart is publishable (no changes or version bumped)' + value: ${{ jobs.validate-chart-versions.outputs.integrations-hub-publishable }} openhands-publishable: description: 'Whether openhands chart is publishable (no changes or version bumped)' value: ${{ jobs.validate-chart-versions.outputs.openhands-publishable }} @@ -47,6 +50,7 @@ jobs: image-loader-publishable: ${{ steps.validate.outputs.image-loader-publishable }} automation-publishable: ${{ steps.validate.outputs.automation-publishable }} plugin-directory-publishable: ${{ steps.validate.outputs.plugin-directory-publishable }} + integrations-hub-publishable: ${{ steps.validate.outputs.integrations-hub-publishable }} openhands-publishable: ${{ steps.validate.outputs.openhands-publishable }} openhands-secrets-publishable: ${{ steps.validate.outputs.openhands-secrets-publishable }} diff --git a/charts/integrations-hub/Chart.yaml b/charts/integrations-hub/Chart.yaml new file mode 100644 index 00000000..90102fd4 --- /dev/null +++ b/charts/integrations-hub/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: integrations-hub +description: OpenHands Integrations Hub - Agent context layer with managed connectors and MCP integrations +type: application +version: 0.1.0 +appVersion: "0.1.0" +dependencies: + - name: postgresql + version: 15.x.x + repository: https://charts.bitnami.com/bitnami + condition: postgresql.enabled diff --git a/charts/integrations-hub/templates/_env.yaml b/charts/integrations-hub/templates/_env.yaml new file mode 100644 index 00000000..c3685515 --- /dev/null +++ b/charts/integrations-hub/templates/_env.yaml @@ -0,0 +1,192 @@ +{{- /* + Environment variables consumed by the Integrations Hub backend + (Python/FastAPI on the `development` branch of OpenHands/integrations-hub). + + The backend reads its config in backend/app/config.py using the prefix + `INTHUB_*`, with legacy unprefixed fallbacks for backwards compatibility: + + INTHUB_POSTGRES_URL fallback: POSTGRES_URL, DATABASE_URL + INTHUB_DISABLE_AUTH (bool, defaults to false) + INTHUB_OPENHANDS_BASE_URL fallback: OPENHANDS_BASE_URL + INTHUB_OPENHANDS_AUTH_COOKIE_NAME fallback: OPENHANDS_AUTH_COOKIE_NAME + INTHUB_INTERNAL_AUTH_SECRET fallback: INTERNAL_AUTH_SECRET, AUTH_SECRET, NEXTAUTH_SECRET + INTHUB_CRON_SECRET fallback: CRON_SECRET + APP_ADMIN_EMAILS (use the unprefixed name; the prefixed + form is JSON-decoded by the SDK env + parser and would require list syntax) + INTHUB_CREDENTIAL_ENCRYPTION_KEY fallback: CREDENTIAL_ENCRYPTION_KEY + INTHUB_STATIC_DIR set in the Dockerfile to /app/out + INTHUB_ROOT_PATH sub-path mount; defaulted by the + Dockerfile to /integrations-hub and + overridable via .Values.rootPath + + In addition, backend/app/oauth_flow.py reads: + AUTH_REDIRECT_PROXY_URL stable origin for OAuth callback proxy + AUTH_URL / NEXTAUTH_URL preview-deployment callback target +*/}} +{{- define "integrations-hub.env.defaults" }} +# Database connection -- backend reads INTHUB_POSTGRES_URL first; we build +# it from the configured host/port/user/db plus the password secret using +# Kubernetes $(VAR_NAME) expansion. If .Values.database.url is set it wins. +{{- if .Values.database.url }} +- name: INTHUB_POSTGRES_URL + value: {{ .Values.database.url | quote }} +{{- else }} +- name: INTHUB_DB_HOST + value: {{ .Values.database.host | quote }} +- name: INTHUB_DB_PORT + value: {{ .Values.database.port | quote }} +- name: INTHUB_DB_USER + value: {{ .Values.database.user | quote }} +- name: INTHUB_DB_NAME + value: {{ .Values.database.name | quote }} +- name: INTHUB_DB_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.database.secretName }} + key: {{ .Values.database.secretKey }} +- name: INTHUB_POSTGRES_URL + value: "postgresql://$(INTHUB_DB_USER):$(INTHUB_DB_PASS)@$(INTHUB_DB_HOST):$(INTHUB_DB_PORT)/$(INTHUB_DB_NAME)" +{{- end }} + +# OpenHands identity provider used to validate session cookies / API keys. +{{- $openhandsBaseUrl := .Values.openhands.baseUrl }} +{{- if and (not $openhandsBaseUrl) .Values.global }} +{{- if .Values.global.ingress }} +{{- if .Values.global.ingress.host }} +{{- $host := .Values.global.ingress.host }} +{{- if and .Values.global.ingress.prefixWithBranch .Values.global.branchSanitized }} +{{- $host = printf "%s.%s" .Values.global.branchSanitized .Values.global.ingress.host }} +{{- end }} +{{- $openhandsBaseUrl = printf "https://%s" $host }} +{{- end }} +{{- end }} +{{- end }} +{{- if $openhandsBaseUrl }} +- name: INTHUB_OPENHANDS_BASE_URL + value: {{ $openhandsBaseUrl | quote }} +{{- end }} +{{- if .Values.openhands.authCookieName }} +- name: INTHUB_OPENHANDS_AUTH_COOKIE_NAME + value: {{ .Values.openhands.authCookieName | quote }} +{{- end }} + +# Local-dev auth bypass. Defaults to false; the FastAPI app will reject +# non-loopback requests when this is true, so it should never be enabled +# in staging/production. +- name: INTHUB_DISABLE_AUTH + value: {{ .Values.disableAuth | default false | quote }} + +# Sub-path mount for the FastAPI app. The Dockerfile already defaults +# INTHUB_ROOT_PATH to /integrations-hub so the image works behind the +# default ingress out of the box; this block re-emits the value so chart +# overrides flow through. Rendered unconditionally (including when +# .Values.rootPath is "") so that an empty override actually clears the +# image default instead of silently inheriting it. +- name: INTHUB_ROOT_PATH + value: {{ .Values.rootPath | default "" | quote }} + +# Credential encryption key (required: signs/encrypts stored OAuth tokens +# and other provider secrets at rest). +- name: INTHUB_CREDENTIAL_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.credentialEncryption.secretName }} + key: {{ .Values.credentialEncryption.secretKey }} + +# Internal service-to-service secret. Optional in SPA-only K8s deployments +# (the backend validates the OpenHands session cookie directly when no +# Next.js proxy is in front of it) but required if any internal caller +# attaches the X-Integrations-Hub-Internal-Secret header. +{{- if and .Values.auth .Values.auth.internalSecretName }} +- name: INTHUB_INTERNAL_AUTH_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.auth.internalSecretName }} + key: {{ .Values.auth.internalSecretKey | default "internal-auth-secret" }} +{{- end }} + +# Admin allowlist for /api/admin/* routes. Use the unprefixed name -- +# INTHUB_APP_ADMIN_EMAILS goes through the SDK env parser which expects +# JSON list syntax. +{{- if .Values.admin.emails }} +- name: APP_ADMIN_EMAILS + value: {{ .Values.admin.emails | quote }} +{{- end }} + +# Cron secret -- required by /api/cron/expire-grants. +- name: INTHUB_CRON_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.cron.secretName }} + key: {{ .Values.cron.secretKey }} + +# OAuth preview-deployment proxy (optional). When set, service OAuth +# callbacks register/exchange against the stable origin and then forward +# back to the preview deployment captured in OAuth state. +{{- if .Values.openhands.redirectProxyUrl }} +- name: AUTH_REDIRECT_PROXY_URL + value: {{ .Values.openhands.redirectProxyUrl | quote }} +{{- end }} +{{- if .Values.openhands.authUrl }} +- name: AUTH_URL + value: {{ .Values.openhands.authUrl | quote }} +{{- end }} + +{{- if .Values.datadog.enabled }} +# Datadog APM +- name: DD_AGENT_HOST + value: "datadog-agent.all-hands-system.svc.cluster.local" +- name: DD_TRACE_AGENT_PORT + value: "8126" +- name: DD_SERVICE + value: {{ .Values.datadog.serviceName | quote }} +- name: DD_ENV + value: {{ .Values.datadog.env | quote }} +- name: DD_TRACE_ENABLED + value: "true" +{{- end }} +{{- end }} + +{{/* + integrations-hub.env — Deduplicated environment variable list. + + This wrapper renders the default env vars from "integrations-hub.env.defaults", + then removes any entries whose name conflicts with a key in .Values.env, + and finally appends the .Values.env overrides. The result is a clean list + with no duplicate names, which prevents: + - Helm warnings about duplicate env vars + - Strategic Merge Patch conflicts during helm upgrade + ("The order in patch list doesn't match $setElementOrder list") + + How it works: + 1. Render "integrations-hub.env.defaults" via include (evaluates all conditionals) + 2. Parse the rendered YAML list into Go objects with fromYamlArray + 3. Filter out any default entries whose name appears in .Values.env + 4. Append .Values.env entries (user overrides always win) + 5. Re-render the deduplicated list with toYaml +*/}} +{{- define "integrations-hub.env" }} +{{- $defaults := include "integrations-hub.env.defaults" . | fromYamlArray }} +{{- /* Build a lookup dict of override keys for O(1) membership checks */}} +{{- $overrideKeys := dict }} +{{- if .Values.env }} +{{- range $key, $_ := .Values.env }} +{{- $_ := set $overrideKeys $key true }} +{{- end }} +{{- end }} +{{- /* Keep only default entries that are NOT overridden by .Values.env */}} +{{- $filtered := list }} +{{- range $entry := $defaults }} +{{- if not (hasKey $overrideKeys (get $entry "name")) }} +{{- $filtered = append $filtered $entry }} +{{- end }} +{{- end }} +{{- /* Append user overrides from .Values.env (these take precedence) */}} +{{- if .Values.env }} +{{- range $key, $value := .Values.env }} +{{- $filtered = append $filtered (dict "name" $key "value" ($value | toString)) }} +{{- end }} +{{- end }} +{{- $filtered | toYaml }} +{{- end }} diff --git a/charts/integrations-hub/templates/deployment.yaml b/charts/integrations-hub/templates/deployment.yaml new file mode 100644 index 00000000..13fd3aa1 --- /dev/null +++ b/charts/integrations-hub/templates/deployment.yaml @@ -0,0 +1,160 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: integrations-hub + labels: + app: integrations-hub +spec: + replicas: {{ .Values.deployment.replicas }} + selector: + matchLabels: + app: integrations-hub + template: + metadata: + labels: + app: integrations-hub + spec: + serviceAccountName: {{ .Values.serviceAccount.name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + {{- if and .Values.database.createDatabaseUser (not .Values.gcp.dbInstance) }} + # Create database and user in PostgreSQL (non-GCP only) + # For GCP Cloud SQL, database and user are created via Terraform in the infra repo + - name: create-db-user + image: postgres:14 + env: + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.superuserSecretName }} + key: {{ .Values.database.superuserSecretKey }} + - name: DB_HOST + value: {{ .Values.database.host | quote }} + - name: DB_PORT + value: {{ .Values.database.port | quote }} + - name: DB_NAME + value: {{ .Values.database.name | quote }} + - name: DB_USER + value: {{ .Values.database.user | quote }} + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.secretName }} + key: {{ .Values.database.secretKey }} + command: + - sh + - -c + - | + echo "Waiting for PostgreSQL at $DB_HOST to be ready..." + for i in $(seq 1 60); do + # Try connecting and capture any error + if psql -h "$DB_HOST" -p "$DB_PORT" -U postgres -d postgres -c "SELECT 1;" 2>&1; then + echo "PostgreSQL is up!" + + echo "Creating the database $DB_NAME if it doesn't exist..." + psql -h "$DB_HOST" -p "$DB_PORT" -U postgres -d postgres --set=ON_ERROR_STOP=1 <<'SQL' + \set db_name `printf %s "$DB_NAME"` + + SELECT format('CREATE DATABASE %I', :'db_name') + WHERE NOT EXISTS ( + SELECT 1 FROM pg_database WHERE datname = :'db_name' + ) + \gexec + SQL + + echo "Creating the user $DB_USER if it doesn't exist..." + psql -h "$DB_HOST" -p "$DB_PORT" -U postgres -d "$DB_NAME" --set=ON_ERROR_STOP=1 <<'SQL' + \set db_name `printf %s "$DB_NAME"` + \set db_user `printf %s "$DB_USER"` + \set db_password `printf %s "$DB_PASSWORD"` + + SELECT format('CREATE USER %I WITH PASSWORD %L', :'db_user', :'db_password') + WHERE NOT EXISTS ( + SELECT 1 FROM pg_roles WHERE rolname = :'db_user' + ) + \gexec + + GRANT ALL PRIVILEGES ON DATABASE :"db_name" TO :"db_user"; + GRANT USAGE ON SCHEMA public TO :"db_user"; + GRANT CREATE ON SCHEMA public TO :"db_user"; + ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO :"db_user"; + SQL + + echo "Database and user creation complete." + exit 0 + fi + echo "Waiting for PostgreSQL... ($i/60)" + sleep 5 + done + echo "PostgreSQL did not become available in time." + exit 1 + {{- else if .Values.postgresql.enabled }} + # Wait for the service's own PostgreSQL subchart to be ready + - name: wait-for-postgres + image: bitnamilegacy/postgresql:latest + command: ['sh', '-c'] + args: + - | + DB_HOST="{{ .Values.database.host }}" + echo "Waiting for PostgreSQL at $DB_HOST to be ready..." + until PGPASSWORD=$INTHUB_DB_PASS psql -h $DB_HOST -p {{ .Values.database.port }} -U {{ .Values.database.user }} -c '\q' > /dev/null 2>&1; do + echo "PostgreSQL is unavailable - sleeping for 2 seconds" + sleep 2 + done + echo "PostgreSQL is up and running!" + env: + {{- include "integrations-hub.env" . | nindent 8 }} + {{- end }} + # Run database migrations before starting the main container + - name: migrate + image: '{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}' + command: ["alembic", "upgrade", "head"] + env: + {{- include "integrations-hub.env" . | nindent 8 }} + containers: + - name: integrations-hub + imagePullPolicy: Always + image: '{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}' + ports: + - containerPort: {{ .Values.service.port | default 8000 }} + resources: + {{- toYaml .Values.deployment.resources | nindent 12 }} + # Health endpoint lives at /api/health on the FastAPI app + # (backend/app/routers/health.py). The leading prefix matches + # `.Values.rootPath` -- i.e. /integrations-hub/api/health by + # default, plain /api/health when rootPath is unset -- because + # the backend mounts all routes under INTHUB_ROOT_PATH. Keeping + # the probe URL in sync with the Helm value prevents probes + # from 404'ing whenever the chart's mount path changes. + # There is no separate /ready endpoint, so readiness reuses the + # same probe path. + {{- $healthPath := printf "%s/api/health" (.Values.rootPath | default "") }} + startupProbe: + httpGet: + path: {{ $healthPath | quote }} + port: {{ .Values.service.port | default 8000 }} + failureThreshold: {{ .Values.probes.startup.failureThreshold }} + periodSeconds: {{ .Values.probes.startup.periodSeconds }} + livenessProbe: + httpGet: + path: {{ $healthPath | quote }} + port: {{ .Values.service.port | default 8000 }} + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + readinessProbe: + httpGet: + path: {{ $healthPath | quote }} + port: {{ .Values.service.port | default 8000 }} + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + env: + {{- include "integrations-hub.env" . | nindent 8 }} diff --git a/charts/integrations-hub/templates/service-account.yaml b/charts/integrations-hub/templates/service-account.yaml new file mode 100644 index 00000000..753a8c3a --- /dev/null +++ b/charts/integrations-hub/templates/service-account.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name }} + labels: + app: integrations-hub + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/integrations-hub/templates/service.yaml b/charts/integrations-hub/templates/service.yaml new file mode 100644 index 00000000..c4f35a2e --- /dev/null +++ b/charts/integrations-hub/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: integrations-hub + labels: + app: integrations-hub +spec: + ports: + - port: 80 + targetPort: {{ .Values.service.port | default 8000 }} + protocol: TCP + name: http + selector: + app: integrations-hub diff --git a/charts/integrations-hub/values.yaml b/charts/integrations-hub/values.yaml new file mode 100644 index 00000000..2c39bb10 --- /dev/null +++ b/charts/integrations-hub/values.yaml @@ -0,0 +1,202 @@ +image: + repository: ghcr.io/openhands/integrations-hub + tag: latest + +imagePullSecrets: [] + +deployment: + replicas: 1 + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + +securityContext: + runAsUser: 42420 + runAsGroup: 42420 + runAsNonRoot: true + +serviceAccount: + create: true + name: integrations-hub-sa + annotations: {} + +probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + liveness: + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + readiness: + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + +# Service configuration +service: + # Public base URL where this service is reachable + # Example: https://integrations.example.com or https://domain/integrations + baseUrl: "" + # Port the container listens on (8000 for Python/FastAPI backend) + port: 8000 + +# Path prefix the application is mounted under. Must match: +# 1. The image's `NEXT_PUBLIC_BASE_PATH` build arg (baked into the SPA +# bundle at image-build time -- see the project Dockerfile). +# 2. The parent chart's `templates/ingress-integrations-hub.yaml` path. +# +# Wired to the FastAPI container as `INTHUB_ROOT_PATH`, which causes +# `backend/app/main.py::build_app` to mount every route -- including the +# SPA static files and `/api/health` -- under this prefix. Container +# probes below also honour the prefix automatically. +# +# Set to an empty string to serve the app from the cluster root (only +# makes sense for a dedicated subdomain image where the SPA was built +# without `NEXT_PUBLIC_BASE_PATH`). +rootPath: "/integrations-hub" + +# Bypass authentication for local development only. When true the FastAPI +# app rejects non-loopback requests and uses a synthetic dev@localhost +# owner; never enable this in staging/production. +# Exposed to the container as INTHUB_DISABLE_AUTH. +disableAuth: false + +# OpenHands identity provider configuration. The backend validates session +# cookies / API keys against GET /api/v1/users/me. +openhands: + # Base URL of the OpenHands instance. Falls back to the global ingress + # host when left empty (see templates/_env.yaml). + # Backend env var: INTHUB_OPENHANDS_BASE_URL + baseUrl: "" + # Cookie name set by the OpenHands identity provider session. + # Backend env var: INTHUB_OPENHANDS_AUTH_COOKIE_NAME + authCookieName: "keycloak_auth" + # Stable-origin URL used as the OAuth callback proxy for preview + # deployments (mirrors the AUTH_REDIRECT_PROXY_URL pattern used by + # Auth.js on Vercel). Leave empty to register callbacks against the + # request host directly. + # Backend env var: AUTH_REDIRECT_PROXY_URL + redirectProxyUrl: "" + # Optional callback target for preview deployments; used when + # redirectProxyUrl is set and oauth_flow.py needs to forward back to + # the originating deployment. + # Backend env var: AUTH_URL + authUrl: "" + +# Credential encryption key for stored provider credentials. +# Generate with: openssl rand -base64 32 +# Backend env var: INTHUB_CREDENTIAL_ENCRYPTION_KEY +credentialEncryption: + # Secret containing the encryption key + secretName: "integrations-hub-encryption-secret" + secretKey: "encryption-key" + +# Optional internal service-to-service auth secret. Required only when a +# Next.js proxy in front of FastAPI injects X-Integrations-Hub-Internal-Secret; +# in pure SPA + FastAPI K8s deployments this is unused. +# Backend env var: INTHUB_INTERNAL_AUTH_SECRET +auth: + # Leave empty to skip wiring INTHUB_INTERNAL_AUTH_SECRET into the pod. + internalSecretName: "" + internalSecretKey: "internal-auth-secret" + +# Admin configuration +# Backend env var: APP_ADMIN_EMAILS (comma-separated) +admin: + # Comma-separated list of admin email addresses. + emails: "chuck@openhands.dev,chuck@all-hands.dev,graham@openhands.dev,graham@all-hands.dev" + +# PostgreSQL database configuration +# The backend expects a single URL via INTHUB_POSTGRES_URL. The chart +# constructs it from the components below using Kubernetes $(VAR_NAME) +# expansion (see templates/_env.yaml). Alternatively, set `database.url` +# directly to bypass the components and supply a full connection string. +database: + # Optional pre-built connection URL (e.g. postgresql://user:pass@host:port/db). + # When set, the chart uses this verbatim and the host/port/user/name/secret + # fields below are not consulted. + # Backend env var: INTHUB_POSTGRES_URL + url: "" + # Full hostname of the PostgreSQL server + # Examples: + # - Cloud SQL: "my-project:us-central1:my-instance" + # - In-cluster: "integrations-hub-postgresql" + # - Feature env: "integrations-hub-feature-branch-postgresql" + host: "" + port: "5432" + user: "integrations_hub_user" + name: "integrations_hub" + # Secret containing the database password + secretName: "integrations-hub-db-secret" + secretKey: "db-password" + # Use an existing secret instead of auto-generating one + # When true, the chart will not create the db secret + existingSecret: false + # Create database and user in an existing PostgreSQL instance (non-GCP only) + # When true, runs an init container to create the database and user + # Useful when sharing a PostgreSQL instance with other services + # NOTE: For GCP Cloud SQL, database and user are created via Terraform in the infra repo + # This setting is ignored when gcp.dbInstance is set + createDatabaseUser: false + # Secret containing the postgres superuser password (for creating the service user) + # Only used when createDatabaseUser=true and gcp.dbInstance is not set + superuserSecretName: "postgres-password" + superuserSecretKey: "password" + # Connection pool settings + poolSize: 10 + maxOverflow: 5 + +# GCP Cloud SQL (leave empty for non-GCP) +# When gcp.dbInstance is set, the database and user must be pre-created via Terraform +gcp: + dbInstance: "" + project: "" + region: "" + +# Datadog configuration +datadog: + enabled: false + env: "dev" + serviceName: "integrations-hub" + +# Cron job configuration +cron: + # Secret for cron job authentication + secretName: "integrations-hub-cron-secret" + secretKey: "cron-secret" + +# Additional environment variables passed verbatim to the container. +# Entries here win over the defaults computed in templates/_env.yaml, so +# they are the right escape hatch for one-off knobs the backend reads +# directly (for example NEXTAUTH_URL or service-specific OAuth secrets +# such as NOTION_CLIENT_ID/NOTION_CLIENT_SECRET). +env: {} + +# PostgreSQL subchart configuration (for ephemeral/feature environments) +# When enabled, deploys an in-cluster PostgreSQL instance +# Service name will be "{release-name}-postgresql", secret name also "{release-name}-postgresql" +postgresql: + enabled: false + auth: + username: postgres + database: integrations_hub + primary: + persistence: + enabled: false + initdb: + scriptsConfigMap: "" + service: + ports: + postgresql: 5432 + image: + repository: bitnamilegacy/postgresql + +global: + security: + # This allows using the bitnamilegacy image repo + allowInsecureImages: true diff --git a/charts/openhands/Chart.yaml b/charts/openhands/Chart.yaml index 888c5efe..88389364 100644 --- a/charts/openhands/Chart.yaml +++ b/charts/openhands/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 description: OpenHands is an AI-driven autonomous software engineer name: openhands appVersion: cloud-1.29.1 -version: 0.7.16 +version: 0.7.17 maintainers: - name: rbren - name: xingyao @@ -51,3 +51,7 @@ dependencies: repository: oci://ghcr.io/all-hands-ai/helm-charts version: 0.1.1 condition: plugin-directory.enabled + - name: integrations-hub + repository: oci://ghcr.io/all-hands-ai/helm-charts + version: 0.1.0 + condition: integrations-hub.enabled diff --git a/charts/openhands/templates/ingress-integrations-hub.yaml b/charts/openhands/templates/ingress-integrations-hub.yaml new file mode 100644 index 00000000..01936788 --- /dev/null +++ b/charts/openhands/templates/ingress-integrations-hub.yaml @@ -0,0 +1,59 @@ +{{- /* + Ingress entry for the integrations-hub service. + + The integrations-hub backend is a single FastAPI container that serves + BOTH the Next.js SPA bundle and the /api/* routes from one image. When + `(index .Values "integrations-hub" "rootPath")` is set (default + `/integrations-hub`), the FastAPI app mounts every route under that + prefix -- so `/integrations-hub/api/health`, `/integrations-hub/api/mcp`, + and the SPA HTML / `/_next/...` assets all live behind the same path. + + That means this ingress only needs ONE rule covering the prefix; we do + not need a separate /api/integrations-hub rewrite, and we deliberately + do not configure a `nginx.ingress.kubernetes.io/rewrite-target` -- the + backend handles its own prefix. + + The path is read from the integrations-hub subchart so the ingress + rule, container probes, and the SPA bundle's baked-in + `NEXT_PUBLIC_BASE_PATH` always agree. +*/}} +{{- if and .Values.ingress.enabled (index .Values "integrations-hub" "enabled") }} +{{- $rootPath := index .Values "integrations-hub" "rootPath" | default "/integrations-hub" }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: openhands-integrations-hub-ingress + annotations: + {{- if .Values.ingress.root.annotations }} + {{ .Values.ingress.root.annotations | toYaml | nindent 4 }} + {{- else }} + {{ .Values.ingress.annotations | toYaml | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.ingress.class }} + {{- if .Values.tls.enabled }} + tls: + - hosts: + {{- if .Values.ingress.prefixWithBranch }} + - {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} + {{- else }} + - {{ .Values.ingress.host }} + {{- end }} + secretName: app-all-hands-{{ .Values.tls.env }}-tls + {{- end }} + rules: + {{- if .Values.ingress.prefixWithBranch }} + - host: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} + {{- else }} + - host: {{ .Values.ingress.host }} + {{- end }} + http: + paths: + - path: {{ $rootPath | quote }} + pathType: Prefix + backend: + service: + name: integrations-hub + port: + number: 80 +{{- end }} diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 59a881f6..e215feef 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -965,3 +965,144 @@ crdCheck: limits: memory: 128Mi location: "" + +# Integrations Hub - Agent context layer with managed connectors and MCP integrations +integrations-hub: + enabled: false + + image: + repository: ghcr.io/openhands/integrations-hub + # tag: set via helm args or override + + imagePullSecrets: [] + + deployment: + replicas: 1 + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + + securityContext: + runAsUser: 42420 + runAsGroup: 42420 + runAsNonRoot: true + + serviceAccount: + create: true + name: integrations-hub-sa + annotations: {} + + probes: + startup: + failureThreshold: 30 + periodSeconds: 10 + liveness: + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + readiness: + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + + # Service configuration + service: + baseUrl: "" + # Port the FastAPI container listens on (matches the integrations-hub + # Dockerfile's `EXPOSE 8000` on the development branch). + port: 8000 + + # Path prefix that the hub is mounted under. This single value drives + # three things at once and they MUST agree: + # - The parent chart's `templates/ingress-integrations-hub.yaml` + # routing rule (host-based ingress + this path). + # - The FastAPI container's `INTHUB_ROOT_PATH` env var, which causes + # `backend/app/main.py::build_app` to mount every route under the + # prefix. + # - The Next.js SPA's `NEXT_PUBLIC_BASE_PATH`, which is baked into + # the image at build time (default `/integrations-hub` in the + # project Dockerfile). Override the image's build arg if you set a + # different rootPath here. + rootPath: "/integrations-hub" + + # Bypass authentication for local development only -- never enable in + # staging/production. Exposed as INTHUB_DISABLE_AUTH. + disableAuth: false + + # OpenHands identity provider used to validate session cookies / API keys. + openhands: + # Backend env var: INTHUB_OPENHANDS_BASE_URL (falls back to the global + # ingress host when left empty -- see charts/integrations-hub/templates/_env.yaml). + baseUrl: "" + # Backend env var: INTHUB_OPENHANDS_AUTH_COOKIE_NAME + authCookieName: "keycloak_auth" + # Stable-origin URL used as the OAuth callback proxy for preview + # deployments. Backend env var: AUTH_REDIRECT_PROXY_URL. + redirectProxyUrl: "" + # Optional callback target for preview deployments paired with + # redirectProxyUrl. Backend env var: AUTH_URL. + authUrl: "" + + # Credential encryption key configuration. + # Backend env var: INTHUB_CREDENTIAL_ENCRYPTION_KEY. + credentialEncryption: + secretName: "integrations-hub-encryption-secret" + secretKey: "encryption-key" + + # Optional internal service-to-service auth secret. Only needed when a + # Next.js proxy in front of FastAPI injects X-Integrations-Hub-Internal-Secret; + # in pure SPA + FastAPI K8s deployments this is unused. Leave + # internalSecretName empty to skip wiring INTHUB_INTERNAL_AUTH_SECRET. + auth: + internalSecretName: "" + internalSecretKey: "internal-auth-secret" + + # Admin allowlist for /api/admin/* routes. + # Backend env var: APP_ADMIN_EMAILS (comma-separated). + admin: + emails: "" + + # PostgreSQL database configuration. The backend reads a single + # connection URL via INTHUB_POSTGRES_URL; the chart builds it from the + # components below using $(VAR_NAME) expansion in templates/_env.yaml. + # Set `database.url` to bypass that builder and supply a pre-formed URL. + database: + url: "" + host: "" + port: "5432" + user: "integrations_hub_user" + name: "integrations_hub" + secretName: "integrations-hub-db-secret" + secretKey: "db-password" + existingSecret: false + createDatabaseUser: false + superuserSecretName: "postgres-password" + superuserSecretKey: "password" + + # GCP Cloud SQL (leave empty for non-GCP) + gcp: + dbInstance: "" + project: "" + region: "" + + # Datadog configuration + datadog: + enabled: false + env: "dev" + serviceName: "integrations-hub" + + # Cron job configuration + cron: + secretName: "integrations-hub-cron-secret" + secretKey: "cron-secret" + + # Env vars passed directly to the container + env: {} + + # PostgreSQL subchart - disabled when using parent's PostgreSQL + postgresql: + enabled: false