├── .github ├── pull_request_template.md └── workflows │ ├── pr-checks.yml │ ├── publish-helm-charts.yml │ └── validate-chart-versions.yml ├── .gitignore ├── LICENSE ├── README.md ├── charts ├── image-loader │ ├── Chart.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── daemonset.yaml │ │ ├── node-overprovisioner.yaml │ │ └── priority-class.yaml │ └── values.yaml ├── openhands │ ├── .gitignore │ ├── Chart.lock │ ├── Chart.yaml │ ├── README.md │ ├── example-values.yaml │ ├── templates │ │ ├── _env.yaml │ │ ├── certificate.yaml │ │ ├── common-room-sync-cronjob.yaml │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── enrich-user-interaction-cronjob.yaml │ │ ├── gitlab-webhook-verify-cronjob.yaml │ │ ├── hpa.yaml │ │ ├── ingress-integrations.yaml │ │ ├── ingress-mcp.yaml │ │ ├── ingress-root.yaml │ │ ├── integration-events-deployment.yaml │ │ ├── integration-events-service.yaml │ │ ├── keycloak-config-script.yaml │ │ ├── maintenance-tasks-cronjob.yaml │ │ ├── mcp-events-deployment.yaml │ │ ├── mcp-events-service.yaml │ │ ├── migrate-db.job.yaml │ │ ├── monitoring.yaml │ │ ├── proactive-convo-clean-cronjob.yaml │ │ ├── resend-sync-cronjob.yaml │ │ ├── service-account.yaml │ │ ├── service.yaml │ │ ├── tlsstore.yaml │ │ └── user-waitlist-configmap.yaml │ ├── values.runtime-example.yaml │ └── values.yaml └── runtime-api │ ├── Chart.lock │ ├── Chart.yaml │ ├── templates │ ├── _env.yaml │ ├── _helpers.tpl │ ├── cleanup-cronjob.yaml │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── create-db-user-job.yaml │ ├── db-cleanup-cronjob.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── ingress-base-configmap.yaml │ ├── ingress.yaml │ ├── migrate-db.job.yaml │ ├── monitoring.yaml │ ├── pod-status-logger-cronjob.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── warm-runtimes-configmap.yaml │ └── warm-runtimes-cronjob.yaml │ └── values.yaml └── docs ├── ARCHITECTURE.md ├── assets ├── fig1.d2 └── fig1.svg └── build-diagrams.sh /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Helm Chart Checklist 5 | 6 | 7 | 8 | - [ ] I have updated the `version` field in `Chart.yaml` for each modified chart 9 | - [ ] I have tested the chart upgrade path from the previous version 10 | - [ ] I have verified backwards compatibility with existing values.yaml configurations 11 | - [ ] I have updated the chart's README.md if there are any breaking changes or new required values 12 | 13 | ## Additional Notes 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Checks 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'charts/**' 7 | 8 | jobs: 9 | validate-chart-versions: 10 | uses: ./.github/workflows/validate-chart-versions.yml 11 | with: 12 | base_ref: origin/${{ github.base_ref }} 13 | enforce_version_bump: true 14 | 15 | lint-and-test: 16 | runs-on: ubuntu-latest 17 | needs: [validate-chart-versions] 18 | 19 | strategy: 20 | matrix: 21 | chart: 22 | - name: runtime-api 23 | path: charts/runtime-api 24 | - name: image-loader 25 | path: charts/image-loader 26 | - name: openhands 27 | path: charts/openhands 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | with: 33 | fetch-depth: 0 34 | 35 | - name: Set up Helm 36 | uses: azure/setup-helm@v3 37 | with: 38 | version: 'latest' 39 | 40 | - name: Lint ${{ matrix.chart.name }} chart 41 | run: | 42 | echo "Testing ${{ matrix.chart.name }} chart" 43 | echo "Updating dependencies for ${{ matrix.chart.name }}" 44 | helm dependency update ${{ matrix.chart.path }} 45 | 46 | echo "Running helm lint for ${{ matrix.chart.name }}" 47 | helm lint ${{ matrix.chart.path }} 48 | if [ $? -ne 0 ]; then 49 | echo "Helm lint failed for ${{ matrix.chart.name }}" 50 | exit 1 51 | fi 52 | echo "Helm lint passed for ${{ matrix.chart.name }}" 53 | 54 | - name: Template ${{ matrix.chart.name }} chart 55 | run: | 56 | echo "Running helm template for ${{ matrix.chart.name }}" 57 | helm template ${{ matrix.chart.path }} --debug 58 | if [ $? -ne 0 ]; then 59 | echo "Helm template failed for ${{ matrix.chart.name }}" 60 | exit 1 61 | fi 62 | echo "Helm template passed for ${{ matrix.chart.name }}" -------------------------------------------------------------------------------- /.github/workflows/publish-helm-charts.yml: -------------------------------------------------------------------------------- 1 | name: Publish Helm Charts 2 | 3 | on: 4 | # Run on all pushes to main 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'charts/**' 10 | # Manual trigger 11 | workflow_dispatch: 12 | 13 | jobs: 14 | validate-chart-versions: 15 | uses: ./.github/workflows/validate-chart-versions.yml 16 | with: 17 | base_ref: HEAD~1 18 | enforce_version_bump: true 19 | 20 | test-charts: 21 | runs-on: ubuntu-latest 22 | needs: [validate-chart-versions] 23 | 24 | strategy: 25 | matrix: 26 | chart: 27 | - name: runtime-api 28 | path: charts/runtime-api 29 | - name: image-loader 30 | path: charts/image-loader 31 | - name: openhands 32 | path: charts/openhands 33 | 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Set up Helm 41 | uses: azure/setup-helm@v3 42 | with: 43 | version: 'latest' 44 | 45 | - name: Test ${{ matrix.chart.name }} chart with default values 46 | run: | 47 | echo "Testing ${{ matrix.chart.name }} chart with default values" 48 | echo "Updating dependencies for ${{ matrix.chart.name }}" 49 | helm dependency update ${{ matrix.chart.path }} 50 | 51 | echo "Running helm lint for ${{ matrix.chart.name }}" 52 | helm lint ${{ matrix.chart.path }} 53 | if [ $? -ne 0 ]; then 54 | echo "Helm lint test failed for ${{ matrix.chart.name }}" 55 | exit 1 56 | fi 57 | echo "Helm lint test passed for ${{ matrix.chart.name }}" 58 | 59 | echo "Running helm template for ${{ matrix.chart.name }}" 60 | helm template ${{ matrix.chart.path }} --debug 61 | if [ $? -ne 0 ]; then 62 | echo "Helm template test failed for ${{ matrix.chart.name }}" 63 | exit 1 64 | fi 65 | echo "Helm template test passed for ${{ matrix.chart.name }}" 66 | 67 | publish-charts: 68 | runs-on: ubuntu-latest 69 | needs: test-charts 70 | permissions: 71 | contents: read 72 | packages: write 73 | 74 | strategy: 75 | matrix: 76 | chart: 77 | - name: runtime-api 78 | path: charts/runtime-api 79 | - name: image-loader 80 | path: charts/image-loader 81 | - name: openhands 82 | path: charts/openhands 83 | 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v3 87 | with: 88 | fetch-depth: 0 89 | 90 | - name: Set up Helm 91 | uses: azure/setup-helm@v3 92 | with: 93 | version: 'latest' 94 | 95 | - name: Extract chart version and prepare repository 96 | id: chart_info 97 | run: | 98 | VERSION=$(grep '^version:' ${{ matrix.chart.path }}/Chart.yaml | awk '{print $2}') 99 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 100 | echo "Using chart version: ${VERSION}" 101 | 102 | # Convert repository owner to lowercase for GHCR 103 | REPO_OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') 104 | echo "REPO_OWNER=${REPO_OWNER}" >> $GITHUB_OUTPUT 105 | echo "Using repository owner: ${REPO_OWNER}" 106 | 107 | - name: Publish ${{ matrix.chart.name }} chart to GHCR 108 | uses: appany/helm-oci-chart-releaser@v0.4.2 109 | with: 110 | name: ${{ matrix.chart.name }} 111 | repository: helm-charts 112 | path: ${{ matrix.chart.path }} 113 | registry: ghcr.io/${{ steps.chart_info.outputs.REPO_OWNER }} 114 | registry_username: ${{ github.actor }} 115 | registry_password: ${{ secrets.GITHUB_TOKEN }} 116 | update_dependencies: 'true' 117 | tag: ${{ steps.chart_info.outputs.VERSION }} 118 | -------------------------------------------------------------------------------- /.github/workflows/validate-chart-versions.yml: -------------------------------------------------------------------------------- 1 | name: Validate Chart Versions 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | base_ref: 7 | description: 'Base reference for comparison (e.g., HEAD~1 for push, origin/main for PR)' 8 | required: false 9 | type: string 10 | default: 'HEAD~1' 11 | enforce_version_bump: 12 | description: 'Whether to enforce version bump for changed charts' 13 | required: false 14 | type: boolean 15 | default: true 16 | 17 | jobs: 18 | validate-chart-versions: 19 | runs-on: ubuntu-latest 20 | env: 21 | YQ_VERSION: 'v4.47.1' 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 # Need full history for comparison 28 | 29 | - name: Install yq 30 | run: | 31 | sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/${{ env.YQ_VERSION }}/yq_linux_amd64 32 | sudo chmod +x /usr/local/bin/yq 33 | 34 | - name: Check for chart changes and validate version bumps 35 | run: | 36 | echo "Comparing against: ${{ inputs.base_ref }}" 37 | 38 | # Get list of changed files 39 | CHANGED_FILES=$(git diff --name-only ${{ inputs.base_ref }}...HEAD) 40 | echo "Changed files:" 41 | echo "$CHANGED_FILES" 42 | 43 | # Dynamically discover all chart directories 44 | CHART_DIRS=() 45 | for dir in charts/*/; do 46 | if [ -f "${dir}Chart.yaml" ]; then 47 | # Remove trailing slash 48 | chart_dir="${dir%/}" 49 | CHART_DIRS+=("$chart_dir") 50 | fi 51 | done 52 | 53 | echo "Found chart directories: ${CHART_DIRS[@]}" 54 | 55 | # Track if any validation failed 56 | VALIDATION_FAILED=false 57 | 58 | for chart_dir in "${CHART_DIRS[@]}"; do 59 | # Check if any files in this chart directory were changed 60 | if echo "$CHANGED_FILES" | grep -q "^${chart_dir}/"; then 61 | echo "Changes detected in ${chart_dir}, validating version..." 62 | 63 | # Get current version 64 | CURRENT_VERSION=$(yq e '.version' "${chart_dir}/Chart.yaml") 65 | echo "Current version: $CURRENT_VERSION" 66 | 67 | # Get base version 68 | BASE_VERSION=$(git show ${{ inputs.base_ref }}:"${chart_dir}/Chart.yaml" | yq e '.version' -) 69 | echo "Base version: $BASE_VERSION" 70 | 71 | # Check if version bump is required 72 | if [ "${{ inputs.enforce_version_bump }}" = "true" ]; then 73 | # Compare versions (simple string comparison for now) 74 | if [ "$CURRENT_VERSION" = "$BASE_VERSION" ]; then 75 | echo "❌ ERROR: Chart ${chart_dir} has changes but version was not bumped!" 76 | echo " Current version: $CURRENT_VERSION" 77 | echo " Base version: $BASE_VERSION" 78 | echo " Please increment the version in ${chart_dir}/Chart.yaml" 79 | VALIDATION_FAILED=true 80 | else 81 | echo "✅ Version bumped for ${chart_dir}: $BASE_VERSION → $CURRENT_VERSION" 82 | fi 83 | else 84 | # Just inform about version status 85 | if [ "$CURRENT_VERSION" = "$BASE_VERSION" ]; then 86 | echo "⚠️ WARNING: Chart ${chart_dir} has changes but version was not bumped" 87 | echo " Current version: $CURRENT_VERSION" 88 | echo " Base version: $BASE_VERSION" 89 | else 90 | echo "✅ Version changed for ${chart_dir}: $BASE_VERSION → $CURRENT_VERSION" 91 | fi 92 | fi 93 | else 94 | echo "No changes in ${chart_dir}, skipping version validation" 95 | fi 96 | done 97 | 98 | if [ "$VALIDATION_FAILED" = true ]; then 99 | echo "" 100 | echo "Version validation failed! Please bump the chart versions for all modified charts." 101 | exit 1 102 | fi 103 | 104 | echo "All chart version validations passed!" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | charts/**/charts 2 | 3 | **/.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # PolyForm Free Trial License 1.0.0 2 | 3 | ## Acceptance 4 | 5 | In order to get any license under these terms, you must agree 6 | to them as both strict obligations and conditions to all 7 | your licenses. 8 | 9 | ## Copyright License 10 | 11 | The licensor grants you a copyright license for the software 12 | to do everything you might do with the software that would 13 | otherwise infringe the licensor's copyright in it for any 14 | permitted purpose. However, you may only make changes or 15 | new works based on the software according to [Changes and New 16 | Works License](#changes-and-new-works-license), and you may 17 | not distribute copies of the software. 18 | 19 | ## Changes and New Works License 20 | 21 | The licensor grants you an additional copyright license to 22 | make changes and new works based on the software for any 23 | permitted purpose. 24 | 25 | ## Patent License 26 | 27 | The licensor grants you a patent license for the software that 28 | covers patent claims the licensor can license, or becomes able 29 | to license, that you would infringe by using the software. 30 | 31 | ## Fair Use 32 | 33 | You may have "fair use" rights for the software under the 34 | law. These terms do not limit them. 35 | 36 | ## Free Trial 37 | 38 | Use of the software for more than 30 days per calendar year is not allowed without a commercial license. 39 | 40 | ## No Other Rights 41 | 42 | These terms do not allow you to sublicense or transfer any of 43 | your licenses to anyone else, or prevent the licensor from 44 | granting licenses to anyone else. These terms do not imply 45 | any other licenses. 46 | 47 | ## Patent Defense 48 | 49 | If you make any written claim that the software infringes or 50 | contributes to infringement of any patent, your patent license 51 | for the software granted under these terms ends immediately. If 52 | your company makes such a claim, your patent license ends 53 | immediately for work on behalf of your company. 54 | 55 | ## Violations 56 | 57 | If you violate any of these terms, or do anything with the 58 | software not covered by your licenses, all your licenses 59 | end immediately. 60 | 61 | ## No Liability 62 | 63 | ***As far as the law allows, the software comes as is, without 64 | any warranty or condition, and the licensor will not be liable 65 | to you for any damages arising out of these terms or the use 66 | or nature of the software, under any kind of legal claim.*** 67 | 68 | ## Definitions 69 | 70 | The **licensor** is the individual or entity offering these 71 | terms, and the **software** is the software the licensor makes 72 | available under these terms. 73 | 74 | **You** refers to the individual or entity agreeing to these 75 | terms. 76 | 77 | **Your company** is any legal entity, sole proprietorship, 78 | or other kind of organization that you work for, plus all 79 | organizations that have control over, are under the control of, 80 | or are under common control with that organization. **Control** 81 | means ownership of substantially all the assets of an entity, 82 | or the power to direct its management and policies by vote, 83 | contract, or otherwise. Control can be direct or indirect. 84 | 85 | **Your licenses** are all the licenses granted to you for the 86 | software under these terms. 87 | 88 | **Use** means anything you do with the software requiring one 89 | of your licenses. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenHands Cloud 2 | > [!WARNING] 3 | > This software is licensed under the [Polyform Free Trial License](./LICENSE). This is **NOT** an open source license. Usage is limited to 30 days per calendar year without a commercial license. If you would like to use it beyond 30 days, please [contact us](https://www.all-hands.dev/contact). 4 | 5 | > [!WARNING] 6 | > This is a work in progress and may contain bugs, incomplete features, or breaking changes. 7 | 8 | This repository contains Helm charts for installing OpenHands Cloud 9 | in your own Kubernetes cluster. These charts are also used to drive 10 | the official, public version of OpenHands Cloud at 11 | [app.all-hands.dev](https://app.all-hands.dev). 12 | 13 | You may also want to check out the MIT-licensed [OpenHands](https://github.com/All-Hands-AI/OpenHands) 14 | 15 | Issues in this repository are for the OpenHands Cloud app and 16 | for the Helm charts here. Issues with OpenHands itself should be 17 | opened in the [core OpenHands repository](github.com/All-Hands-AI/OpenHands). 18 | 19 | # Installation 20 | 21 | See [charts/openhands/README.md](./charts/openhands/README.md) for 22 | full instructions. 23 | -------------------------------------------------------------------------------- /charts/image-loader/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: image-loader 3 | description: A Helm chart for loading images on nodes using a DaemonSet with configurable runtime class 4 | version: 0.1.0 5 | appVersion: "1.0.0" 6 | -------------------------------------------------------------------------------- /charts/image-loader/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "image-loader.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "image-loader.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "image-loader.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "image-loader.labels" -}} 37 | helm.sh/chart: {{ include "image-loader.chart" . }} 38 | {{ include "image-loader.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "image-loader.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "image-loader.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /charts/image-loader/templates/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: {{ include "image-loader.fullname" . }} 5 | labels: 6 | {{- include "image-loader.labels" . | nindent 4 }} 7 | spec: 8 | updateStrategy: 9 | type: RollingUpdate 10 | rollingUpdate: 11 | maxUnavailable: 100% 12 | selector: 13 | matchLabels: 14 | {{- include "image-loader.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | labels: 18 | {{- include "image-loader.selectorLabels" . | nindent 8 }} 19 | spec: 20 | {{- with .Values.securityContext }} 21 | securityContext: 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | {{- with .Values.nodeSelector }} 25 | nodeSelector: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | {{- with .Values.affinity }} 29 | affinity: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | {{- with .Values.tolerations }} 33 | tolerations: 34 | {{- toYaml . | nindent 8 }} 35 | {{- end }} 36 | containers: 37 | - name: {{ .Chart.Name }} 38 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 39 | imagePullPolicy: {{ .Values.image.pullPolicy }} 40 | command: ["sleep", "infinity"] 41 | resources: 42 | {{- toYaml .Values.resources | nindent 12 }} 43 | -------------------------------------------------------------------------------- /charts/image-loader/templates/node-overprovisioner.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "image-loader.fullname" . }}-node-overprovisioner 5 | labels: 6 | {{- include "image-loader.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: node-overprovisioner 8 | spec: 9 | replicas: {{ .Values.nodeOverprovisioner.replicas | default 1 }} 10 | selector: 11 | matchLabels: 12 | {{- include "image-loader.selectorLabels" . | nindent 6 }} 13 | app.kubernetes.io/component: node-overprovisioner 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "image-loader.selectorLabels" . | nindent 8 }} 18 | app.kubernetes.io/component: node-overprovisioner 19 | spec: 20 | priorityClassName: {{ .Values.nodeOverprovisioner.priorityClassName | default "node-overprovisioner-priority" }} 21 | {{- with .Values.securityContext }} 22 | securityContext: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | {{- with .Values.nodeSelector }} 26 | nodeSelector: 27 | {{- toYaml . | nindent 8 }} 28 | {{- end }} 29 | {{- with .Values.affinity }} 30 | affinity: 31 | {{- toYaml . | nindent 8 }} 32 | {{- end }} 33 | {{- with .Values.tolerations }} 34 | tolerations: 35 | {{- toYaml . | nindent 8 }} 36 | {{- end }} 37 | containers: 38 | - name: pause 39 | image: k8s.gcr.io/pause:3.2 40 | resources: 41 | requests: 42 | cpu: {{ .Values.nodeOverprovisioner.resources.requests.cpu | default "4" }} 43 | memory: {{ .Values.nodeOverprovisioner.resources.requests.memory | default "4Gi" }} 44 | limits: 45 | cpu: {{ .Values.nodeOverprovisioner.resources.limits.cpu | default "4" }} 46 | memory: {{ .Values.nodeOverprovisioner.resources.limits.memory | default "4Gi" }} 47 | -------------------------------------------------------------------------------- /charts/image-loader/templates/priority-class.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scheduling.k8s.io/v1 2 | kind: PriorityClass 3 | metadata: 4 | name: {{ .Values.nodeOverprovisioner.priorityClassName }} 5 | value: -10 6 | globalDefault: false 7 | description: "This priority class is used for the node-overprovisioner deployment to ensure it gets evicted for other workloads." 8 | -------------------------------------------------------------------------------- /charts/image-loader/values.yaml: -------------------------------------------------------------------------------- 1 | image: 2 | repository: ghcr.io/all-hands-ai/runtime 3 | tag: 0.9.6-nikolaik 4 | pullPolicy: Always 5 | 6 | runtimeClass: sysbox-runc 7 | 8 | resources: 9 | requests: 10 | cpu: 100m 11 | memory: 128Mi 12 | limits: 13 | cpu: 100m 14 | memory: 128Mi 15 | 16 | # Security context settings for all pods 17 | securityContext: 18 | runAsUser: 1000 19 | runAsNonRoot: true 20 | 21 | nodeSelector: 22 | sysbox-install: "yes" 23 | 24 | tolerations: 25 | - key: "sysbox-runtime" 26 | operator: "Equal" 27 | value: "not-running" 28 | effect: "NoSchedule" 29 | affinity: {} 30 | 31 | nodeOverprovisioner: 32 | replicas: 10 33 | priorityClassName: node-overprovisioner-priority 34 | resources: 35 | requests: 36 | cpu: 2500m 37 | memory: 7500Mi 38 | limits: 39 | cpu: 2500m 40 | memory: 7500Mi 41 | -------------------------------------------------------------------------------- /charts/openhands/.gitignore: -------------------------------------------------------------------------------- 1 | charts/ 2 | my-values.yaml 3 | -------------------------------------------------------------------------------- /charts/openhands/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: clickhouse 3 | repository: oci://registry-1.docker.io/bitnamicharts 4 | version: 9.2.5 5 | - name: keycloak 6 | repository: oci://registry-1.docker.io/bitnamicharts 7 | version: 24.5.2 8 | - name: langfuse 9 | repository: https://langfuse.github.io/langfuse-k8s 10 | version: 1.2.13 11 | - name: litellm-helm 12 | repository: oci://ghcr.io/berriai 13 | version: 0.1.664 14 | - name: minio 15 | repository: https://charts.min.io/ 16 | version: 5.0.10 17 | - name: postgresql 18 | repository: https://charts.bitnami.com/bitnami 19 | version: 15.5.38 20 | - name: redis 21 | repository: oci://registry-1.docker.io/bitnamicharts 22 | version: 20.3.0 23 | - name: runtime-api 24 | repository: oci://ghcr.io/all-hands-ai/helm-charts 25 | version: 0.1.9 26 | digest: sha256:0eb3fcb98ec8a1aa3d6c2ba5e317942cbc2030a35440836ad6aec63de0dfb11e 27 | generated: "2025-08-21T14:15:03.196589052Z" 28 | -------------------------------------------------------------------------------- /charts/openhands/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: OpenHands is an AI-driven autonomous software engineer 3 | name: openhands 4 | appVersion: 0.54.2 5 | version: 0.1.31 6 | maintainers: 7 | - name: rbren 8 | - name: xingyao 9 | - name: tofarr 10 | dependencies: 11 | - name: clickhouse 12 | repository: oci://registry-1.docker.io/bitnamicharts 13 | version: 9.2.5 14 | condition: clickhouse.enabled 15 | - name: keycloak 16 | version: 24.5.2 17 | repository: oci://registry-1.docker.io/bitnamicharts 18 | condition: keycloak.enabled 19 | - name: langfuse 20 | repository: https://langfuse.github.io/langfuse-k8s 21 | version: 1.2.13 22 | condition: langfuse.enabled 23 | - name: litellm-helm 24 | repository: oci://ghcr.io/berriai 25 | version: 0.1.781 26 | condition: litellm-helm.enabled 27 | - name: minio 28 | version: 5.0.10 29 | condition: filestore.ephemeral 30 | repository: https://charts.min.io/ 31 | - name: postgresql 32 | version: 15.x.x 33 | repository: https://charts.bitnami.com/bitnami 34 | condition: postgresql.enabled 35 | - name: redis 36 | version: 20.3.0 37 | repository: oci://registry-1.docker.io/bitnamicharts 38 | condition: redis.enabled 39 | - name: runtime-api 40 | repository: oci://ghcr.io/all-hands-ai/helm-charts 41 | version: 0.1.15 42 | condition: runtime-api.enabled 43 | -------------------------------------------------------------------------------- /charts/openhands/README.md: -------------------------------------------------------------------------------- 1 | # OpenHands Cloud Helm Chart 2 | 3 | This Helm chart deploys the complete OpenHands stack, including all required dependencies. It's designed to be a one-stop solution for deploying OpenHands in a Kubernetes environment. 4 | 5 | ## Prerequisites 6 | 7 | - Kubernetes 1.19+ 8 | - Helm 3.2.0+ 9 | - Ingress controller (recommended: Traefik) 10 | - A TLS solution for certificates (recommended: cert-manager) 11 | 12 | ### Hardware prerequisites 13 | 14 | - Profile the application's resource usage (CPU, memory) to establish the minimum required specifications for the cluster. 15 | 16 | ## Configuration 17 | 18 | See the [values.yaml](values.yaml) file for the full list of configurable parameters. 19 | Make sure to update all values marked with "REQUIRED" comments. 20 | 21 | ### TLS and Certificate Configuration 22 | 23 | The chart supports two methods for TLS configuration: 24 | 25 | 1. **Standard TLS**: Enable with `tls.enabled: true`. This uses a certificate with the name format `app-all-hands-{env}-tls`. 26 | 27 | 2. **Wildcard Certificate**: Enable with `certificate.enabled: true`. This creates a cert-manager Certificate resource that can use a wildcard domain (e.g., `*.prod-runtime.all-hands.dev`). This is particularly useful for runtime environments where you need a wildcard certificate. 28 | 29 | 3. **TLSStore for Traefik**: Enable with `tlsStore.enabled: true`. This creates a Traefik TLSStore resource that configures the default certificate for Traefik to use. When combined with a wildcard certificate, this allows Traefik to use the wildcard certificate for all TLS connections. 30 | 31 | For runtime environments, see the [values.runtime-example.yaml](values.runtime-example.yaml) file for an example configuration using a wildcard certificate and TLSStore. 32 | 33 | An [example-values.yaml](example-values.yaml) file is also provided as a starting point 34 | for your own configuration. This example file contains the minimum set of values you need 35 | to override when deploying the chart with the default included services 36 | (without using external data stores). Remember to update the domain names and other 37 | environment-specific values in the example file before using it. 38 | 39 | ## Installation 40 | 41 | ### Initial setup 42 | 43 | #### 1. Create the openhands namespace 44 | 45 | If you want to use a different namespace, you'll need to change the `-n` option 46 | in all the commands below. 47 | 48 | ```bash 49 | kubectl create namespace openhands 50 | ``` 51 | 52 | #### 2. Create a secret for your LLM 53 | 54 | We'll assume Anthropic here, but you can set any env vars you'll need to connect to your LLM, 55 | including e.g. OpenAPI keys, or AWS keys for Bedrock models. You can use any env var names 56 | you want--we'll reference them again below in our LiteLLM setup. 57 | 58 | ```bash 59 | kubectl create secret generic litellm-env-secrets -n openhands \ 60 | --from-literal=ANTHROPIC_API_KEY= 61 | ``` 62 | 63 | #### 3. Create required secrets 64 | 65 | There are several databases and other services that need a secret or admin password to function. 66 | We'll create a single `$GLOBAL_SECRET` to drive all of these, but we recommend using 67 | [SOPS](https://github.com/getsops/sops) or another solution for managing Kubernetes secrets long-term. 68 | 69 | If you are using your own LiteLLM instance, see the NOTE. 70 | 71 | ```bash 72 | 73 | export GLOBAL_SECRET=`head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32` 74 | 75 | kubectl create secret generic jwt-secret -n openhands --from-literal=jwt-secret=$GLOBAL_SECRET 76 | 77 | kubectl create secret generic keycloak-realm -n openhands \ 78 | --from-literal=realm-name=allhands \ 79 | --from-literal=provider-name=email \ 80 | --from-literal=server-url=http://keycloak \ 81 | --from-literal=client-id=allhands \ 82 | --from-literal=client-secret=$GLOBAL_SECRET \ 83 | --from-literal=smtp-password= 84 | 85 | kubectl create secret generic keycloak-admin -n openhands \ 86 | --from-literal=admin-password=$GLOBAL_SECRET 87 | 88 | kubectl create secret generic postgres-password -n openhands \ 89 | --from-literal=username=postgres \ 90 | --from-literal=password=$GLOBAL_SECRET \ 91 | --from-literal=postgres-password=$GLOBAL_SECRET 92 | 93 | kubectl create secret generic redis -n openhands \ 94 | --from-literal=redis-password=$GLOBAL_SECRET 95 | 96 | # NOTE: if you are using your own LiteLLM instance, then change $GLOBAL_SECRET to your LiteLLM API Key 97 | kubectl create secret generic lite-llm-api-key -n openhands \ 98 | --from-literal=lite-llm-api-key=$GLOBAL_SECRET 99 | 100 | kubectl create secret generic langfuse-salt -n openhands \ 101 | --from-literal=salt=$GLOBAL_SECRET 102 | 103 | kubectl create secret generic langfuse-nextauth -n openhands \ 104 | --from-literal=nextauth-secret=$GLOBAL_SECRET 105 | 106 | kubectl create secret generic clickhouse-password -n openhands \ 107 | --from-literal=password=$GLOBAL_SECRET 108 | 109 | kubectl create secret generic admin-password -n openhands \ 110 | --from-literal=admin-password=$GLOBAL_SECRET 111 | 112 | # NOTE: these need to be the same value 113 | # TODO: merge these two secrets 114 | kubectl create secret generic default-api-key -n openhands \ 115 | --from-literal=default-api-key=$GLOBAL_SECRET 116 | kubectl create secret generic sandbox-api-key -n openhands \ 117 | --from-literal=sandbox-api-key=$GLOBAL_SECRET 118 | ``` 119 | 120 | You should now have these secrets in the openhands namespace: 121 | 122 | ```bash 123 | kubectl get secret -n openhands 124 | 125 | NAME TYPE DATA AGE 126 | clickhouse-password Opaque 1 13s 127 | default-api-key Opaque 1 7s 128 | jwt-secret Opaque 1 44s 129 | langfuse-nextauth Opaque 1 18s 130 | langfuse-salt Opaque 1 23s 131 | lite-llm-api-key Opaque 1 28s 132 | litellm-env-secrets Opaque 1 2m8s 133 | postgres-password Opaque 3 39s 134 | redis Opaque 1 35s 135 | sandbox-api-key Opaque 1 3s 136 | ``` 137 | 138 | #### 4. Create a helm values file 139 | 140 | Copy the example-values.yaml file to a file name of your choice. For the purposes of this document we will call this file `site-values.yaml` 141 | 142 | We will update this file in the following sections and there will likely be customizations for your environment (see comments in the file for more information on common changes). 143 | 144 | ### Enabling IDP Authentication 145 | 146 | You'll need to set up GitHub, GitLab, and/or BitBucket as an auth provider. We're working on email-based 147 | authentication as well. 148 | 149 | #### GitHub 150 | 151 | 1. Create a GitHub App: 152 | 153 | - Go to your GitHub organization settings or personal settings. 154 | - Navigate to "Developer settings" > "GitHub Apps" > "New GitHub App". 155 | - In the "GitHub App name" field, enter a descriptive name (e.g., Openhands app). 156 | - Add your "Homepage URL" `https://openhands.example.com`. 157 | - Add the "Callback URL" `https://auth.openhands.example.com/realms/allhands/broker/github/endpoint`. 158 | - In "Permissions": 159 | 160 | - Open "Account permissions" and select "Access: Read-only" to "Email addresses". 161 | - In "Repository permissions" add "Access: Read and Write" to "Projects". 162 | 163 | - If you want to get webhooks: 164 | 165 | - Generate a webhook secret `export WEBHOOK_SECRET=head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32`. 166 | - Check the "Active" checkbox. 167 | - Set the "Webhook URL" `https://openhands.example.com/integration/github/events`. 168 | - Go to "Permissions", click in "Organization permissions" and add "Access: Read-only" to "Events". 169 | - Set the "Secret" to `$WEBHOOK_SECRET`. 170 | 171 | - Create the App. 172 | - Generate a private key which will download a private key file. 173 | - Note the App ID, Client ID, and Client Secret provided by GitHub. Remember to save the Client Secret and your private key, because it is only displayed once. 174 | 175 | 2. Create a GitHub App secret with the following structure: 176 | This secret contains the GitHub App configuration information from your GitHub account. 177 | You can create it using kubectl: 178 | 179 | ```bash 180 | kubectl create secret generic github-app -n openhands \ 181 | --from-literal=app-id= \ 182 | --from-literal=webhook-secret=$WEBHOOK_SECRET \ 183 | --from-literal=client-id= \ 184 | --from-literal=client-secret= \ 185 | --from-file=private-key= 186 | ``` 187 | 188 | 3. Update site-values.yaml file: 189 | 190 | ```yaml 191 | github: 192 | # Set this to true if you are using GitHub as your identity provider 193 | enabled: true 194 | ``` 195 | 196 | #### GitLab 197 | 198 | 1. Create a GitLab Application: 199 | 200 | - Go to your GitLab Group. 201 | - Navigate to "Settings" > "Applications" 202 | - Set the "Redirect URI" to `https://auth.openhands.example.com/realms/openhands/broker/gitlab/endpoint` 203 | - Select the following scopes: api, read_user, write_repository, openid, email, profile 204 | - Note the Client ID and Client Secret provided by GitLab 205 | 206 | 2. Create a GitLab App secret: 207 | 208 | ```bash 209 | kubectl create secret generic gitlab-app -n openhands \ 210 | --from-literal=client-id= \ 211 | --from-literal=client-secret= \ 212 | ``` 213 | 214 | 3. Update site-values.yaml file: 215 | 216 | ```yaml 217 | gitlab: 218 | # Set this to true if you are using GitLab as your identity provider 219 | enabled: true 220 | ``` 221 | 222 | #### BitBucket 223 | 224 | 1. Create a BitBucket OAuth Consumer: 225 | 226 | - Go to your Workspace Settings. 227 | - Select "OAuth consumers" in the left pane 228 | - Set the "Callback URL" to `https://auth.openhands.example.com/realms/openhands/broker/bitbucket/endpoint` 229 | - Select the following permissions: account:read, workspace:read, projects:write, repositories:write, pullrequests:write, issues:write, snippets:read, pipelines:read 230 | - Note the Client ID and Client Secret provided by BitBucket 231 | 232 | 2. Create a BitBucket App secret: 233 | 234 | ```bash 235 | kubectl create secret generic bitbucket-app -n openhands \ 236 | --from-literal=client-id= \ 237 | --from-literal=client-secret= \ 238 | ``` 239 | 240 | 3. Update site-values.yaml file: 241 | 242 | ```yaml 243 | bitbucket: 244 | # Set this to true if you are using BitBucket as your identity provider 245 | enabled: true 246 | ``` 247 | 248 | When the chart is deployed, a job will run to configure the Keycloak realm with the identity provider credentials you provided. 249 | 250 | ### Install OpenHands 251 | 252 | Now we can install the helm chart. 253 | 254 | ```bash 255 | helm dependency update 256 | helm upgrade --install openhands --namespace openhands oci://ghcr.io/all-hands-ai/helm-charts/openhands -f site-values.yaml 257 | ``` 258 | 259 | This installation won't complete successfully the first time because we need to set up LiteLLM. 260 | 261 | > [!NOTE] 262 | > This process will be automated in the near future. 263 | > 264 | > [!IMPORTANT] 265 | > We recommend using the provided LiteLLM instance rather than bringing your 266 | > own. The provided LiteLLM instance uses an admin key for automated user 267 | > management, which is the most extensively tested scenario. Our automation 268 | > relies on this admin key to create and delete users automatically. 269 | 270 | To set up LiteLLM, first use port-forward to connect: 271 | 272 | ```bash 273 | kubectl port-forward svc/openhands-litellm 4000:4000 -n openhands 274 | ``` 275 | 276 | Next, create a new Team in LiteLLM: 277 | 278 | - Navigate to in your browser. 279 | - login using the username `admin` password $GLOBAL_SECRET (set above). 280 | - go to Teams -> Create New Team. 281 | - Name it whatever you want. 282 | - Get the team id (e.g. `e0a62105-9c6c-4167-b5be-16674a99d502`), and add it to site-values.yaml: 283 | 284 | ```yaml 285 | litellm: 286 | teamId: "" 287 | ``` 288 | 289 | You'll also need to set your model list for LiteLLM, using the LLM secrets you set above: 290 | 291 | ```yaml 292 | litellm: 293 | teamId: "" 294 | 295 | litellm-helm: 296 | proxy_config: 297 | model_list: 298 | - model_name: "prod/claude-sonnet-4-20250514" 299 | litellm_params: 300 | model: "anthropic/claude-sonnet-4-20250514" 301 | api_key: os.environ/ANTHROPIC_API_KEY 302 | ``` 303 | 304 | Finally you will need to set the default LLM model to use in your site-values.yaml. Find the "env:" section below in your site-values.yaml and uncomment the LITELLM_DEFAULT_MODEL. Set "your-model" to one of the models you configured: 305 | 306 | ```yaml 307 | env: 308 | # This var will cause the openhands deploy to create the LLM team name if it doesn't exist already 309 | LITE_LLM_TEAM_NAME: openhands 310 | # replace with your LLM model and uncomment this variable 311 | # LITELLM_DEFAULT_MODEL: "litellm_proxy/" 312 | ``` 313 | 314 | ### Verify your Setup 315 | 316 | Finally, upgrade the release: 317 | 318 | ```bash 319 | helm upgrade --install openhands --namespace openhands oci://ghcr.io/all-hands-ai/helm-charts/openhands -f site-values.yaml 320 | ``` 321 | 322 | You should now be able to see OpenHands running with: 323 | 324 | ```bash 325 | kubectl port-forward svc/openhands-service 3000:3000 -n openhands 326 | ``` 327 | 328 | If you visit `http://localhost:3000` you should see the login screen! 329 | 330 | But we're not done yet... 331 | 332 | ## Setting up DNS and Ingress 333 | 334 | We recommend traefik as an ingress controller. If you're not using traefik, 335 | you can set ingress.class in the objects below. 336 | 337 | You'll also need to point your DNS records to the ingress controller's IP address. 338 | In this example, we'll use `openhands.example.com` as the base domain. 339 | 340 | First, set up a CNAME record pointing `*.openhands.example.com` to your ingress 341 | controller's IP address. 342 | 343 | Next, enable ingress in site-values.yaml: 344 | 345 | ```yaml 346 | ingress: 347 | enabled: true 348 | host: openhands.example.com 349 | keycloak: 350 | url: https://auth.openhands.example.com 351 | ingress: 352 | enabled: true 353 | hostname: auth.openhands.example.com 354 | runtime-api: 355 | ingress: 356 | enabled: true 357 | hostname: runtimes.openhands.example.com 358 | litellm-helm: 359 | ingress: 360 | enabled: true 361 | hosts: 362 | - host: llm-proxy.example.com 363 | paths: 364 | - path: / 365 | pathType: Prefix 366 | ``` 367 | 368 | Upgrade the release: 369 | 370 | ```bash 371 | helm upgrade --install openhands --namespace openhands oci://ghcr.io/all-hands-ai/helm-charts/openhands -f site-values.yaml 372 | ``` 373 | 374 | ## Hardening 375 | 376 | The above configuration should work well for a POC. However, it uses several in-cluster databases, 377 | which creates risk of data loss. 378 | 379 | We recommend at minimum setting up a more permanent Postgres and S3-compatible file store, e.g. 380 | using AWS RDS and AWS S3. 381 | 382 | ### Bring Your Own PostgreSQL 383 | 384 | To use an external PostgreSQL database instead of deploying one with the chart: 385 | 386 | 1. Disable the included PostgreSQL: 387 | 388 | ```yaml 389 | postgresql: 390 | enabled: false 391 | ``` 392 | 393 | 2. Configure the external database connection: 394 | 395 | ```yaml 396 | externalDatabase: 397 | host: your-postgresql-host 398 | port: 5432 399 | database: openhands 400 | existingSecret: postgres-password 401 | # Make sure the secret exists with the correct credentials 402 | # kubectl create secret generic postgres-password \ 403 | # --from-literal=username= \ 404 | # --from-literal=password= 405 | ``` 406 | 407 | 3. Update the Keycloak, LiteLLM, and runtime-api configurations to use the external database: 408 | 409 | ```yaml 410 | keycloak: 411 | externalDatabase: 412 | host: your-postgresql-host 413 | port: 5432 414 | existingSecret: postgres-password 415 | 416 | litellm-helm: 417 | db: 418 | deployStandalone: false 419 | useExisting: true 420 | database: litellm 421 | endpoint: your-postgresql-host 422 | secret: 423 | name: postgres-password 424 | 425 | runtime-api: 426 | postgresql: 427 | auth: 428 | existingSecret: postgres-password 429 | env: 430 | DB_HOST: your-postgresql-host 431 | DB_USER: your-db-username 432 | DB_NAME: runtime_api_db 433 | ``` 434 | 435 | ### Bring Your Own S3-Compatible Storage 436 | 437 | To use an external S3-compatible storage instead of MinIO: 438 | 439 | 1. Disable the ephemeral filestore: 440 | 441 | ```yaml 442 | filestore: 443 | ephemeral: false 444 | ``` 445 | 446 | 2. Configure the S3 connection: 447 | 448 | ```yaml 449 | filestore: 450 | ephemeral: false 451 | bucket: your-bucket-name 452 | endpoint: https://your-s3-endpoint 453 | region: your-s3-region 454 | existingSecret: s3-credentials 455 | # Make sure the secret exists with the correct credentials 456 | # kubectl create secret generic s3-credentials \ 457 | # --from-literal=access-key= \ 458 | # --from-literal=secret-key= 459 | ``` 460 | 461 | ### Bring Your Own Redis 462 | 463 | To use an external Redis instance: 464 | 465 | 1. Disable the included Redis: 466 | 467 | ```yaml 468 | redis: 469 | enabled: false 470 | ``` 471 | 472 | 2. Configure the external Redis connection: 473 | 474 | ```yaml 475 | externalRedis: 476 | host: your-redis-host 477 | port: 6379 478 | existingSecret: redis 479 | # Make sure the secret exists with the correct credentials 480 | # kubectl create secret generic redis \ 481 | # --from-literal=redis-password= 482 | ``` 483 | 484 | ### Storage Class Configuration 485 | 486 | By default, the chart expects a storage class named `standard-rwo`. If you're using EKS, which typically has a `gp2` storage class, you can configure the chart to use it instead: 487 | 488 | ```yaml 489 | runtime-api: 490 | env: 491 | STORAGE_CLASS: "gp2" # Replace with your cluster's storage class name 492 | ``` 493 | 494 | Alternatively, you can create a storage class named `standard-rwo` that uses your cloud provider's block storage: 495 | 496 | ```bash 497 | # For AWS EKS 498 | kubectl apply -f - < with your LLM model and uncomment this variable 9 | # LITELLM_DEFAULT_MODEL: "litellm_proxy/" 10 | 11 | ingress: 12 | enabled: false 13 | host: "app.example.com" 14 | annotations: 15 | {} 16 | # Value should match your Issuer/ClusterIssuer and uncomment if you're using cert-manager for certificates 17 | # cert-manager.io/cluster-issuer: letsencrypt 18 | 19 | tls: 20 | # requires cert for all enabled ingresses 21 | enabled: true 22 | 23 | github: 24 | # Set this to true if you are using GitHub as your identity provider 25 | enabled: false 26 | 27 | gitlab: 28 | # Set this to true if you are using GitLab as your identity provider 29 | enabled: false 30 | 31 | bitbucket: 32 | # Set this to true if you are using BitBucket as your identity provider 33 | enabled: false 34 | 35 | keycloak: 36 | enabled: true 37 | ingress: 38 | enabled: false 39 | hostname: "auth.app.example.com" 40 | annotations: {} 41 | # Value should match your Issuer/ClusterIssuer and uncomment if you're using cert-manager for certificates 42 | # cert-manager.io/cluster-issuer: letsencrypt 43 | 44 | postgresql: 45 | enabled: true 46 | 47 | litellm: 48 | # Set this to true if you are using your own litellm instance 49 | # 50 | # NOTE: We recommend using the provided LiteLLM instance for simplicity and 51 | # because it is the most extensively tested scenario. Our automation uses an 52 | # admin key to do user management for the LiteLLM instance. 53 | enabled: false 54 | url: "https://llm-proxy.example.com" 55 | 56 | litellm-helm: 57 | # Set this to false if you are using your own litellm instance 58 | # 59 | # NOTE: We recommend using the provided LiteLLM instance (enabled: true) for 60 | # simplicity and because it is the most extensively tested scenario. Our 61 | # automation uses an admin key to do user management for the LiteLLM instance. 62 | enabled: true 63 | ingress: 64 | enabled: false 65 | hosts: 66 | - host: llm-proxy.example.com 67 | paths: 68 | - path: / 69 | pathType: Prefix 70 | tls: 71 | - secretName: llm-proxy-tls 72 | hosts: 73 | - llm-proxy.example.com 74 | proxy_config: 75 | environment_variables: 76 | { 77 | "OR_APP_NAME": "OpenHands", 78 | "OR_SITE_URL": "https://docs.all-hands.dev", 79 | } 80 | # Needs to be updated with your models. If you bring your own litellm, then 81 | # just point at that! 82 | # 83 | # NOTE: We recommend using the provided LiteLLM instance for simplicity and 84 | # because it is the most extensively tested scenario. 85 | # 86 | # model_list: 87 | # - model_name: "prod/claude-3-5-sonnet-20241022" 88 | # litellm_params: 89 | # model: "anthropic/claude-3-5-sonnet-20241022" 90 | # api_key: os.environ/ANTHROPIC_API_KEY 91 | # - model_name: "prod/claude-3-7-sonnet-20250219" 92 | # litellm_params: 93 | # model: "anthropic/claude-3-7-sonnet-20250219" 94 | # api_key: os.environ/ANTHROPIC_API_KEY 95 | 96 | runtime-api: 97 | enabled: true 98 | ingress: 99 | enabled: false 100 | host: runtime-api.example.com 101 | annotations: {} 102 | # Value should match your Issuer/ClusterIssuer and uncomment if you're using cert-manager for certificates 103 | # cert-manager.io/cluster-issuer: letsencrypt 104 | env: 105 | RUNTIME_BASE_URL: "runtime.example.com" 106 | # Set to storage class you want to use. 107 | STORAGE_CLASS: "gp2" 108 | 109 | sandbox: 110 | apiHostname: https://runtime-api.example.com 111 | # Tavily API for web search functionality (optional) 112 | # tavily: 113 | # enabled: true 114 | # auth: 115 | # existingSecret: tavily-api-key 116 | -------------------------------------------------------------------------------- /charts/openhands/templates/_env.yaml: -------------------------------------------------------------------------------- 1 | {{- define "openhands.env" }} 2 | - name: RUNTIME 3 | value: remote 4 | - name: OPENHANDS_CONFIG_CLS 5 | value: {{ .Values.appConfig.OPENHANDS_CONFIG_CLS }} 6 | - name: OPENHANDS_GITHUB_SERVICE_CLS 7 | value: {{ .Values.appConfig.OPENHANDS_GITHUB_SERVICE_CLS }} 8 | - name: OPENHANDS_GITLAB_SERVICE_CLS 9 | value: {{ .Values.appConfig.OPENHANDS_GITLAB_SERVICE_CLS }} 10 | {{- if .Values.bitbucket.enabled }} 11 | - name: OPENHANDS_BITBUCKET_SERVICE_CLS 12 | value: {{ .Values.appConfig.OPENHANDS_BITBUCKET_SERVICE_CLS }} 13 | {{- end }} 14 | - name: OPENHANDS_MCP_CONFIG_CLS 15 | value: {{ .Values.appConfig.OPENHANDS_MCP_CONFIG_CLS }} 16 | - name: OPENHANDS_CONVERSATION_VALIDATOR_CLS 17 | value: {{ .Values.appConfig.OPENHANDS_CONVERSATION_VALIDATOR_CLS }} 18 | - name: OPENHANDS_EXPERIMENT_MANAGER_CLS 19 | value: {{ .Values.appConfig.OPENHANDS_EXPERIMENT_MANAGER_CLS }} 20 | - name: LOG_JSON # Configure structured logging for Google Cloud 21 | value: '1' 22 | - name: LOG_JSON_LEVEL_KEY 23 | value: 'severity' 24 | {{- if .Values.appConfig.POSTHOG_CLIENT_KEY }} 25 | - name: POSTHOG_CLIENT_KEY 26 | value: {{ .Values.appConfig.POSTHOG_CLIENT_KEY }} 27 | {{- end }} 28 | 29 | - name: FILE_STORE_PATH 30 | value: {{ .Values.filestore.bucket }} 31 | - name: SANDBOX_KEEP_RUNTIME_ALIVE 32 | value: 'true' 33 | - name: SANDBOX_CLOSE_DELAY 34 | value: "1800" 35 | {{- if .Values.filestore.ephemeral }} 36 | - name: FILE_STORE 37 | value: s3 38 | - name: AWS_ACCESS_KEY_ID 39 | value: {{ (index .Values.minio.svcaccts 0).accessKey }} 40 | - name: AWS_SECRET_ACCESS_KEY 41 | value: {{ (index .Values.minio.svcaccts 0).secretKey }} 42 | - name: AWS_S3_ENDPOINT 43 | value: {{ printf "%s-%s" $.Release.Name "minio:9000" }} 44 | - name: AWS_S3_BUCKET 45 | value: {{ .Values.filestore.bucket }} 46 | - name: AWS_S3_SECURE 47 | value: 'false' 48 | {{- else }} 49 | - name: FILE_STORE 50 | value: {{ .Values.filestore.type }} 51 | {{- end }} 52 | {{- if .Values.runtime.runAsRoot }} 53 | - name: SANDBOX_USER_ID 54 | value: '0' 55 | - name: RUN_AS_OPENHANDS 56 | value: 'false' 57 | {{- else }} 58 | - name: SANDBOX_USER_ID 59 | value: '1000' 60 | - name: RUN_AS_OPENHANDS 61 | value: 'true' 62 | {{- end }} 63 | - name: NO_SETUP 64 | value: 'true' 65 | - name: SANDBOX_RUNTIME_CONTAINER_IMAGE 66 | value: "{{ .Values.runtime.image.repository }}:{{ .Values.runtime.image.tag | default (printf "%s-nikolaik" (.Values.image.tag | default .Chart.AppVersion)) }}" 67 | - name: SANDBOX_REMOTE_RUNTIME_API_URL 68 | value: {{ .Values.sandbox.apiHostname }} 69 | - name: SANDBOX_API_KEY 70 | valueFrom: 71 | secretKeyRef: 72 | name: sandbox-api-key 73 | key: sandbox-api-key 74 | {{- if .Values.sessions.existingSecret }} 75 | - name: JWT_SECRET 76 | valueFrom: 77 | secretKeyRef: 78 | name: {{ .Values.sessions.existingSecret }} 79 | key: jwt-secret 80 | {{- end }} 81 | {{- if .Values.allowedUsers }} 82 | - name: GITHUB_USER_LIST_FILE 83 | value: /app/user-waitlist/user-waitlist.txt 84 | {{- end }} 85 | {{- if .Values.allowedUsersSheet }} 86 | - name: GITHUB_USERS_SHEET_ID 87 | value: {{ .Values.allowedUsersSheet }} 88 | {{- end }} 89 | {{- if .Values.redis.enabled }} 90 | - name: REDIS_HOST 91 | value: {{ .Release.Name }}-redis-master 92 | - name: REDIS_PORT 93 | value: "6379" 94 | - name: REDIS_PASSWORD 95 | valueFrom: 96 | secretKeyRef: 97 | name: {{ .Values.redis.auth.existingSecret }} 98 | key: redis-password 99 | {{- end }} 100 | - name: KEYCLOAK_SERVER_URL 101 | value: {{ .Values.keycloak.url }} 102 | - name: KEYCLOAK_REALM_NAME 103 | valueFrom: 104 | secretKeyRef: 105 | name: keycloak-realm 106 | key: realm-name 107 | - name: KEYCLOAK_PROVIDER_NAME 108 | valueFrom: 109 | secretKeyRef: 110 | name: keycloak-realm 111 | key: provider-name 112 | - name: KEYCLOAK_CLIENT_ID 113 | valueFrom: 114 | secretKeyRef: 115 | name: keycloak-realm 116 | key: client-id 117 | - name: KEYCLOAK_CLIENT_SECRET 118 | valueFrom: 119 | secretKeyRef: 120 | name: keycloak-realm 121 | key: client-secret 122 | - name: KEYCLOAK_SMTP_PASSWORD 123 | valueFrom: 124 | secretKeyRef: 125 | name: keycloak-realm 126 | key: smtp-password 127 | - name: KEYCLOAK_ADMIN_PASSWORD 128 | valueFrom: 129 | secretKeyRef: 130 | name: keycloak-admin 131 | key: admin-password 132 | {{- if .Values.github.enabled }} 133 | - name: GITHUB_APP_PRIVATE_KEY 134 | valueFrom: 135 | secretKeyRef: 136 | name: github-app 137 | key: private-key 138 | - name: GITHUB_APP_WEBHOOK_SECRET 139 | valueFrom: 140 | secretKeyRef: 141 | name: github-app 142 | key: webhook-secret 143 | - name: GITHUB_APP_ID 144 | valueFrom: 145 | secretKeyRef: 146 | name: github-app 147 | key: app-id 148 | - name: GITHUB_APP_CLIENT_ID 149 | valueFrom: 150 | secretKeyRef: 151 | name: github-app 152 | key: client-id 153 | - name: GITHUB_APP_CLIENT_SECRET 154 | valueFrom: 155 | secretKeyRef: 156 | name: github-app 157 | key: client-secret 158 | {{- end }} 159 | {{- if .Values.gitlab.enabled }} 160 | - name: GITLAB_APP_CLIENT_ID 161 | valueFrom: 162 | secretKeyRef: 163 | name: gitlab-app 164 | key: client-id 165 | - name: GITLAB_APP_CLIENT_SECRET 166 | valueFrom: 167 | secretKeyRef: 168 | name: gitlab-app 169 | key: client-secret 170 | {{- end }} 171 | {{- if .Values.bitbucket.enabled }} 172 | - name: BITBUCKET_APP_CLIENT_ID 173 | valueFrom: 174 | secretKeyRef: 175 | name: {{ .Values.bitbucket.auth.existingSecret }} 176 | key: client-id 177 | - name: BITBUCKET_APP_CLIENT_SECRET 178 | valueFrom: 179 | secretKeyRef: 180 | name: {{ .Values.bitbucket.auth.existingSecret }} 181 | key: client-secret 182 | {{- end }} 183 | {{- if and .Values.litellm.enabled .Values.litellm.useDependentInstall }} 184 | - name: LITE_LLM_API_URL 185 | value: http://{{ .Release.Name }}-litellm 186 | {{- else if .Values.litellm.enabled }} 187 | - name: LITE_LLM_API_URL 188 | value: {{ .Values.litellm.url }} 189 | - name: LITE_LLM_TEAM_ID 190 | value: {{ .Values.litellm.teamId }} 191 | - name: LITE_LLM_API_KEY 192 | valueFrom: 193 | secretKeyRef: 194 | name: {{ .Values.litellm.auth.existingSecret }} 195 | key: lite-llm-api-key 196 | {{- end }} 197 | {{- if index .Values "litellm-helm" "enabled" }} 198 | - name: LITE_LLM_API_KEY 199 | valueFrom: 200 | secretKeyRef: 201 | name: {{ index .Values "litellm-helm" "masterkeySecretName" }} 202 | key: {{ index .Values "litellm-helm" "masterkeySecretKey" }} 203 | {{- end }} 204 | {{- if .Values.stripe.enabled }} 205 | - name: STRIPE_API_KEY 206 | valueFrom: 207 | secretKeyRef: 208 | name: {{ .Values.stripe.auth.existingSecret }} 209 | key: stripe-api-key 210 | - name: STRIPE_WEBHOOK_SECRET 211 | valueFrom: 212 | secretKeyRef: 213 | name: {{ .Values.stripe.auth.existingSecret }} 214 | key: stripe-webhook-secret 215 | - name: REQUIRE_PAYMENT 216 | value: "{{ .Values.stripe.requirePayment | default false }}" 217 | {{- end }} 218 | {{- if .Values.tavily.enabled }} 219 | - name: TAVILY_API_KEY 220 | valueFrom: 221 | secretKeyRef: 222 | name: {{ .Values.tavily.auth.existingSecret }} 223 | key: tavily-api-key 224 | {{- end }} 225 | {{- if .Values.ingress.enabled }} 226 | {{- if .Values.ingress.prefixWithBranch }} 227 | - name: WEB_HOST 228 | value: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 229 | - name: AUTH_WEB_HOST 230 | value: {{ .Values.branchSanitized }}.auth.{{ .Values.ingress.host }} 231 | - name: MCP_HOST 232 | value: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 233 | {{- else }} 234 | - name: WEB_HOST 235 | value: {{ .Values.ingress.host }} 236 | - name: AUTH_WEB_HOST 237 | value: auth.{{ .Values.ingress.host }} 238 | - name: MCP_HOST 239 | value: {{ .Values.ingress.host }} 240 | {{- end }} 241 | {{- else }} 242 | - name: WEB_HOST 243 | value: {{ .Values.ingress.host }} 244 | - name: AUTH_WEB_HOST 245 | value: keycloak 246 | - name: MCP_HOST 247 | value: {{ .Values.ingress.host }} 248 | {{- end }} 249 | 250 | {{- if .Values.slack.enabled }} 251 | - name: SLACK_CLIENT_ID 252 | value: "{{ .Values.slack.clientId }}" 253 | - name: SLACK_CLIENT_SECRET 254 | valueFrom: 255 | secretKeyRef: 256 | name: slack-auth 257 | key: client-secret 258 | - name: SLACK_SIGNING_SECRET 259 | valueFrom: 260 | secretKeyRef: 261 | name: slack-auth 262 | key: signing-secret 263 | - name: SLACK_WEBHOOKS_ENABLED 264 | value: "1" 265 | {{- end }} 266 | 267 | {{- if .Values.githubProxy.enabled }} 268 | - name: GITHUB_PROXY 269 | value: "1" 270 | {{- end }} 271 | {{- if .Values.githubProxy.endpointsEnabled }} 272 | - name: GITHUB_PROXY_ENDPOINTS 273 | value: "1" 274 | {{- end }} 275 | 276 | {{- if .Values.debuggingRoutes.enabled }} 277 | - name: ADD_DEBUGGING_ROUTES 278 | value: "1" 279 | {{- end }} 280 | 281 | {{- if .Values.postgresql.enabled }} 282 | - name: DB_HOST 283 | value: "{{ .Release.Name }}-postgresql" 284 | - name: DB_USER 285 | value: "{{ .Values.postgresql.auth.username }}" 286 | - name: DB_NAME 287 | value: "{{ .Values.postgresql.auth.database }}" 288 | {{- end }} 289 | {{- if .Values.env }} 290 | {{- range $key, $value := .Values.env }} 291 | - name: {{ $key }} 292 | value: {{ $value | quote }} 293 | {{- end }} 294 | {{- end }} 295 | {{- if .Values.datadog.enabled }} 296 | # Datadog configuration 297 | - name: DD_AGENT_HOST 298 | value: {{ .Values.datadog.agentHost | quote }} 299 | - name: DD_TRACE_AGENT_PORT 300 | value: "8126" 301 | - name: DD_DOGSTATSD_PORT 302 | value: "8125" 303 | - name: DD_SERVICE 304 | value: {{ .Values.datadog.service | quote }} 305 | - name: DD_ENV 306 | value: {{ .Values.datadog.env | quote }} 307 | - name: DD_LOGS_INJECTION 308 | value: "true" 309 | - name: DD_TRACE_ENABLED 310 | value: "true" 311 | - name: DD_TRACE_SAMPLING_RULES 312 | value: '[{"service":"deploy","name":"fastapi.request","resource":"/integration/*","sample_rate":0.05}, {"service":"deploy","name":"fastapi.request","resource":"/slack/on-*","sample_rate":0.05}]' 313 | {{- end }} 314 | - name: DB_PASS 315 | valueFrom: 316 | secretKeyRef: 317 | name: {{ .Values.postgresql.auth.existingSecret }} 318 | key: password 319 | {{- if .Values.jira.enabled }} 320 | - name: ENABLE_JIRA 321 | value: "true" 322 | - name: JIRA_CLIENT_ID 323 | valueFrom: 324 | secretKeyRef: 325 | name: jira-app 326 | key: client-id 327 | - name: JIRA_CLIENT_SECRET 328 | valueFrom: 329 | secretKeyRef: 330 | name: jira-app 331 | key: client-secret 332 | {{- end }} 333 | 334 | {{- if .Values.jiraDc.enabled }} 335 | - name: ENABLE_JIRA_DC 336 | value: "true" 337 | - name: JIRA_DC_CLIENT_ID 338 | valueFrom: 339 | secretKeyRef: 340 | name: jira-dc-app 341 | key: client-id 342 | - name: JIRA_DC_CLIENT_SECRET 343 | valueFrom: 344 | secretKeyRef: 345 | name: jira-dc-app 346 | key: client-secret 347 | - name: JIRA_DC_BASE_URL 348 | valueFrom: 349 | secretKeyRef: 350 | name: jira-dc-app 351 | key: base-url 352 | {{- end }} 353 | 354 | {{- if .Values.linear.enabled }} 355 | - name: ENABLE_LINEAR 356 | value: "true" 357 | - name: LINEAR_CLIENT_ID 358 | valueFrom: 359 | secretKeyRef: 360 | name: linear-app 361 | key: client-id 362 | - name: LINEAR_CLIENT_SECRET 363 | valueFrom: 364 | secretKeyRef: 365 | name: linear-app 366 | key: client-secret 367 | {{- end }} 368 | {{- end }} 369 | -------------------------------------------------------------------------------- /charts/openhands/templates/certificate.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.certificate.enabled }} 2 | apiVersion: cert-manager.io/v1 3 | kind: Certificate 4 | metadata: 5 | name: {{ .Values.certificate.name }} 6 | namespace: {{ .Values.certificate.namespace }} 7 | spec: 8 | {{- if .Values.certificate.commonName }} 9 | commonName: {{ .Values.certificate.commonName }} 10 | {{- end }} 11 | dnsNames: 12 | {{- if .Values.certificate.domains }} 13 | {{- range .Values.certificate.domains }} 14 | - {{ . | quote }} 15 | {{- end }} 16 | {{- end }} 17 | issuerRef: 18 | group: cert-manager.io 19 | kind: ClusterIssuer 20 | name: {{ .Values.certificate.issuer }} 21 | secretName: {{ .Values.certificate.secretName }} 22 | usages: 23 | - digital signature 24 | - key encipherment 25 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/common-room-sync-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.commonRoomSync.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ .Release.Name }}-common-room-sync 6 | labels: 7 | app: {{ .Release.Name }}-common-room-sync 8 | spec: 9 | schedule: {{ .Values.commonRoomSync.schedule | quote }} 10 | concurrencyPolicy: {{ .Values.commonRoomSync.concurrencyPolicy }} 11 | failedJobsHistoryLimit: {{ .Values.commonRoomSync.failedJobsHistoryLimit }} 12 | successfulJobsHistoryLimit: {{ .Values.commonRoomSync.successfulJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | backoffLimit: {{ .Values.commonRoomSync.backoffLimit }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ .Release.Name }}-common-room-sync 20 | spec: 21 | {{- with .Values.securityContext }} 22 | securityContext: 23 | {{- toYaml . | nindent 12 }} 24 | {{- end }} 25 | serviceAccountName: {{ .Values.serviceAccount.name }} 26 | restartPolicy: OnFailure 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 12 }} 30 | {{- end }} 31 | containers: 32 | - name: {{ .Release.Name }}-common-room-sync 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} 35 | command: ["/bin/sh", "-c"] 36 | args: 37 | - "cd /app && python -m sync.common_room_sync" 38 | resources: 39 | {{- toYaml .Values.commonRoomSync.resources | nindent 16 }} 40 | env: 41 | - name: BATCH_SIZE 42 | value: {{ .Values.commonRoomSync.batchSize | quote }} 43 | - name: MAX_RETRIES 44 | value: {{ .Values.commonRoomSync.maxRetries | quote }} 45 | - name: INITIAL_BACKOFF_SECONDS 46 | value: {{ .Values.commonRoomSync.initialBackoffSeconds | quote }} 47 | - name: MAX_BACKOFF_SECONDS 48 | value: {{ .Values.commonRoomSync.maxBackoffSeconds | quote }} 49 | - name: BACKOFF_FACTOR 50 | value: {{ .Values.commonRoomSync.backoffFactor | quote }} 51 | - name: RATE_LIMIT 52 | value: {{ .Values.commonRoomSync.rateLimit | quote }} 53 | - name: KEYCLOAK_BATCH_SIZE 54 | value: {{ .Values.commonRoomSync.keycloakBatchSize | default "20" | quote }} 55 | - name: COMMON_ROOM_API_KEY 56 | valueFrom: 57 | secretKeyRef: 58 | name: common-room-api-key 59 | key: commonroom-api-key 60 | - name: COMMON_ROOM_DESTINATION_SOURCE_ID 61 | value: "118984" 62 | # Database configuration is included in openhands.env 63 | # GCP configuration (if applicable) 64 | {{- if .Values.gcp }} 65 | {{- if .Values.gcp.dbInstance }} 66 | - name: GCP_DB_INSTANCE 67 | value: "{{ .Values.gcp.dbInstance }}" 68 | - name: GCP_PROJECT 69 | value: "{{ .Values.gcp.project }}" 70 | - name: GCP_REGION 71 | value: "{{ .Values.gcp.region }}" 72 | {{- end }} 73 | {{- end }} 74 | {{- include "openhands.env" . | nindent 16 }} 75 | {{- end }} 76 | -------------------------------------------------------------------------------- /charts/openhands/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.postgresql.enabled (ne .Values.postgresql.primary.initdb.scriptsConfigMap "") -}} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ .Values.postgresql.primary.initdb.scriptsConfigMap }} 6 | data: 7 | init.sql: | 8 | CREATE DATABASE litellm; 9 | GRANT ALL PRIVILEGES ON DATABASE litellm TO {{ .Values.postgresql.auth.username }}; 10 | CREATE DATABASE keycloak; 11 | GRANT ALL PRIVILEGES ON DATABASE keycloak TO {{ .Values.postgresql.auth.username }}; 12 | CREATE DATABASE openhands; 13 | GRANT ALL PRIVILEGES ON DATABASE openhands TO {{ .Values.postgresql.auth.username }}; 14 | CREATE DATABASE postgres_langfuse; 15 | GRANT ALL PRIVILEGES ON DATABASE postgres_langfuse TO {{ .Values.postgresql.auth.username }}; 16 | CREATE DATABASE bitnami_keycloak; 17 | GRANT ALL PRIVILEGES ON DATABASE bitnami_keycloak TO {{ .Values.postgresql.auth.username }}; 18 | CREATE DATABASE runtime_api_db; 19 | GRANT ALL PRIVILEGES ON DATABASE runtime_api_db TO {{ .Values.postgresql.auth.username }}; 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /charts/openhands/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: openhands 5 | labels: 6 | app: openhands 7 | spec: 8 | replicas: {{ .Values.deployment.replicas }} 9 | selector: 10 | matchLabels: 11 | app: openhands 12 | template: 13 | metadata: 14 | labels: 15 | app: openhands 16 | annotations: 17 | {{- if .Values.allowedUsers }} 18 | checksum/user-waitlist: {{ include (print $.Template.BasePath "/user-waitlist-configmap.yaml") . | sha256sum }} 19 | {{- end }} 20 | spec: 21 | terminationGracePeriodSeconds: 60 22 | serviceAccountName: {{ .Values.serviceAccount.name }} 23 | {{- with .Values.securityContext }} 24 | securityContext: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | volumes: 32 | {{- if .Values.allowedUsers }} 33 | - name: user-waitlist 34 | configMap: 35 | name: user-waitlist 36 | {{- end }} 37 | - name: keycloak-config-script 38 | configMap: 39 | name: keycloak-config-script 40 | defaultMode: 0755 41 | initContainers: 42 | - name: keycloak-config 43 | imagePullPolicy: Always 44 | image: '{{.Values.image.repository}}:{{.Values.image.tag | default .Chart.AppVersion }}' 45 | env: 46 | {{- include "openhands.env" . | nindent 8 }} 47 | volumeMounts: 48 | - name: keycloak-config-script 49 | mountPath: /scripts 50 | command: 51 | - sh 52 | - /scripts/keycloak-config.sh 53 | containers: 54 | - name: openhands 55 | imagePullPolicy: Always 56 | image: '{{.Values.image.repository}}:{{.Values.image.tag | default .Chart.AppVersion }}' 57 | command: ["/bin/sh"] 58 | args: 59 | - "-c" 60 | - "exec {{- if .Values.datadog.enabled }} ddtrace-run {{- end }} uvicorn saas_server:app --host 0.0.0.0 --port 3000 --workers {{ .Values.uvicorn.workers }}" 61 | ports: 62 | - containerPort: 3000 63 | resources: 64 | {{- toYaml .Values.deployment.resources | nindent 12 }} 65 | startupProbe: 66 | httpGet: 67 | path: /health 68 | port: 3000 69 | failureThreshold: 30 70 | periodSeconds: 10 71 | livenessProbe: 72 | httpGet: 73 | path: /health 74 | port: 3000 75 | initialDelaySeconds: 10 76 | periodSeconds: 10 77 | failureThreshold: 3 78 | readinessProbe: 79 | httpGet: 80 | path: /ready 81 | port: 3000 82 | initialDelaySeconds: 5 83 | periodSeconds: 10 84 | failureThreshold: 3 85 | volumeMounts: 86 | {{- if .Values.allowedUsers }} 87 | - name: user-waitlist 88 | mountPath: /app/user-waitlist 89 | {{- end }} 90 | env: 91 | {{- include "openhands.env" . | nindent 8 }} 92 | -------------------------------------------------------------------------------- /charts/openhands/templates/enrich-user-interaction-cronjob.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if .Values.enrichUserInteractionData.enabled }} 3 | apiVersion: batch/v1 4 | kind: CronJob 5 | metadata: 6 | name: {{ .Release.Name | trunc 27 }}-enrich-user-interaction 7 | labels: 8 | app: {{ .Release.Name | trunc 27 }}-enrich-user-interaction 9 | spec: 10 | schedule: {{ .Values.enrichUserInteractionData.schedule | quote }} 11 | concurrencyPolicy: {{ .Values.enrichUserInteractionData.concurrencyPolicy }} 12 | failedJobsHistoryLimit: {{ .Values.enrichUserInteractionData.failedJobsHistoryLimit }} 13 | successfulJobsHistoryLimit: {{ .Values.enrichUserInteractionData.successfulJobsHistoryLimit }} 14 | jobTemplate: 15 | spec: 16 | backoffLimit: {{ .Values.enrichUserInteractionData.backoffLimit }} 17 | template: 18 | metadata: 19 | labels: 20 | app: {{ .Release.Name | trunc 27 }}-enrich-user-interaction 21 | spec: 22 | {{- with .Values.securityContext }} 23 | securityContext: 24 | {{- toYaml . | nindent 12 }} 25 | {{- end }} 26 | serviceAccountName: {{ .Values.serviceAccount.name }} 27 | restartPolicy: OnFailure 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 12 }} 31 | {{- end }} 32 | containers: 33 | - name: {{ .Release.Name | trunc 27 }}-enrich-user-interaction 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} 36 | command: ["python", "-m", "sync.enrich_user_interaction_data"] 37 | resources: 38 | {{- toYaml .Values.enrichUserInteractionData.resources | nindent 16 }} 39 | env: 40 | {{- include "openhands.env" . | nindent 16 }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /charts/openhands/templates/gitlab-webhook-verify-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.gitlabWebhookInstallation.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ .Release.Name }}-install-hooks 6 | labels: 7 | app: {{ .Release.Name }}-install-hooks 8 | spec: 9 | schedule: {{ .Values.gitlabWebhookInstallation.schedule | quote }} 10 | concurrencyPolicy: {{ .Values.gitlabWebhookInstallation.concurrencyPolicy }} 11 | failedJobsHistoryLimit: {{ .Values.gitlabWebhookInstallation.failedJobsHistoryLimit }} 12 | successfulJobsHistoryLimit: {{ .Values.gitlabWebhookInstallation.successfulJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | backoffLimit: {{ .Values.gitlabWebhookInstallation.backoffLimit }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ .Release.Name }}-install-hooks 20 | spec: 21 | {{- with .Values.securityContext }} 22 | securityContext: 23 | {{- toYaml . | nindent 12 }} 24 | {{- end }} 25 | serviceAccountName: {{ .Values.serviceAccount.name }} 26 | restartPolicy: OnFailure 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 12 }} 30 | {{- end }} 31 | containers: 32 | - name: {{ .Release.Name }}-install-hooks 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} 35 | command: ["python", "-m", "sync.install_gitlab_webhooks"] 36 | resources: 37 | {{- toYaml .Values.gitlabWebhookInstallation.resources | nindent 16 }} 38 | env: 39 | {{- include "openhands.env" . | nindent 16 }} 40 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: openhands-hpa 6 | labels: 7 | {{- include "openhands.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: openhands 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /charts/openhands/templates/ingress-integrations.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: openhands-integrations-ingress 6 | annotations: 7 | {{- if .Values.ingress.integrations.annotations }} 8 | {{ .Values.ingress.integrations.annotations | toYaml | nindent 4 }} 9 | {{- else }} 10 | {{ .Values.ingress.annotations | toYaml | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | ingressClassName: {{ .Values.ingress.class }} 14 | {{- if .Values.tls.enabled }} 15 | tls: 16 | - hosts: 17 | {{- if .Values.ingress.prefixWithBranch }} 18 | - {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 19 | {{- else }} 20 | - {{ .Values.ingress.host }} 21 | {{- end }} 22 | secretName: app-all-hands-{{ .Values.tls.env }}-tls 23 | {{- end }} 24 | rules: 25 | {{- if .Values.ingress.prefixWithBranch }} 26 | - host: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 27 | {{- else }} 28 | - host: {{ .Values.ingress.host }} 29 | {{- end }} 30 | http: 31 | paths: 32 | - path: /integration/github/events 33 | pathType: Exact 34 | backend: 35 | service: 36 | name: openhands-integrations-service 37 | port: 38 | number: 3000 39 | - path: /integration/gitlab/events 40 | pathType: Exact 41 | backend: 42 | service: 43 | name: openhands-integrations-service 44 | port: 45 | number: 3000 46 | - path: /integration/jira/events 47 | pathType: Exact 48 | backend: 49 | service: 50 | name: openhands-integrations-service 51 | port: 52 | number: 3000 53 | - path: /api/billing/stripe-webhook 54 | pathType: Exact 55 | backend: 56 | service: 57 | name: openhands-integrations-service 58 | port: 59 | number: 3000 60 | - path: /slack 61 | pathType: Prefix 62 | backend: 63 | service: 64 | name: openhands-integrations-service 65 | port: 66 | number: 3000 67 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/ingress-mcp.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: openhands-mcp-ingress 6 | annotations: 7 | {{- if .Values.ingress.mcp.annotations }} 8 | {{ .Values.ingress.mcp.annotations | toYaml | nindent 4 }} 9 | {{- else }} 10 | {{ .Values.ingress.annotations | toYaml | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | ingressClassName: {{ .Values.ingress.class }} 14 | {{- if .Values.tls.enabled }} 15 | tls: 16 | - hosts: 17 | {{- if .Values.ingress.prefixWithBranch }} 18 | - {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 19 | {{- else }} 20 | - {{ .Values.ingress.host }} 21 | {{- end }} 22 | secretName: app-all-hands-{{ .Values.tls.env }}-tls 23 | {{- end }} 24 | rules: 25 | {{- if .Values.ingress.prefixWithBranch }} 26 | - host: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 27 | {{- else }} 28 | - host: {{ .Values.ingress.host }} 29 | {{- end }} 30 | http: 31 | paths: 32 | - path: /mcp/mcp 33 | pathType: Prefix 34 | backend: 35 | service: 36 | name: openhands-mcp-service 37 | port: 38 | number: 3000 39 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/ingress-root.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: openhands-root-ingress 6 | annotations: 7 | {{- if .Values.ingress.root.annotations }} 8 | {{ .Values.ingress.root.annotations | toYaml | nindent 4 }} 9 | {{- else }} 10 | {{ .Values.ingress.annotations | toYaml | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | ingressClassName: {{ .Values.ingress.class }} 14 | {{- if .Values.tls.enabled }} 15 | tls: 16 | - hosts: 17 | {{- if .Values.ingress.prefixWithBranch }} 18 | - {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 19 | {{- else }} 20 | - {{ .Values.ingress.host }} 21 | {{- end }} 22 | secretName: app-all-hands-{{ .Values.tls.env }}-tls 23 | {{- end }} 24 | rules: 25 | {{- if .Values.ingress.prefixWithBranch }} 26 | - host: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 27 | {{- else }} 28 | - host: {{ .Values.ingress.host }} 29 | {{- end }} 30 | http: 31 | paths: 32 | - path: / 33 | pathType: Prefix 34 | backend: 35 | service: 36 | name: openhands-service 37 | port: 38 | number: 3000 39 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/integration-events-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: openhands-integrations 5 | labels: 6 | app: openhands-integrations 7 | spec: 8 | replicas: {{ .Values.integrationEvents.deployment.replicas | default 2 }} 9 | selector: 10 | matchLabels: 11 | app: openhands-integrations 12 | template: 13 | metadata: 14 | labels: 15 | app: openhands-integrations 16 | annotations: 17 | {{- if .Values.allowedUsers }} 18 | checksum/user-waitlist: {{ include (print $.Template.BasePath "/user-waitlist-configmap.yaml") . | sha256sum }} 19 | {{- end }} 20 | spec: 21 | terminationGracePeriodSeconds: 60 22 | serviceAccountName: {{ .Values.serviceAccount.name }} 23 | {{- with .Values.securityContext }} 24 | securityContext: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | volumes: 32 | {{- if .Values.allowedUsers }} 33 | - name: user-waitlist 34 | configMap: 35 | name: user-waitlist 36 | {{- end }} 37 | containers: 38 | - name: openhands-integrations 39 | imagePullPolicy: Always 40 | image: '{{.Values.image.repository}}:{{.Values.image.tag | default .Chart.AppVersion }}' 41 | command: ["/bin/sh"] 42 | args: 43 | - "-c" 44 | - "exec {{- if .Values.datadog.enabled }} ddtrace-run {{- end }} uvicorn saas_server:app --host 0.0.0.0 --port 3000 --workers {{ .Values.integrationEvents.uvicorn.workers | default 2 }}" 45 | ports: 46 | - containerPort: 3000 47 | resources: 48 | {{- toYaml .Values.integrationEvents.deployment.resources | nindent 12 }} 49 | startupProbe: 50 | httpGet: 51 | path: /health 52 | port: 3000 53 | failureThreshold: 30 54 | periodSeconds: 10 55 | livenessProbe: 56 | httpGet: 57 | path: /health 58 | port: 3000 59 | initialDelaySeconds: 10 60 | periodSeconds: 10 61 | failureThreshold: 3 62 | readinessProbe: 63 | httpGet: 64 | path: /health 65 | port: 3000 66 | initialDelaySeconds: 5 67 | periodSeconds: 10 68 | failureThreshold: 3 69 | volumeMounts: 70 | {{- if .Values.allowedUsers }} 71 | - name: user-waitlist 72 | mountPath: /app/user-waitlist 73 | {{- end }} 74 | env: 75 | {{- include "openhands.env" . | nindent 8 }} -------------------------------------------------------------------------------- /charts/openhands/templates/integration-events-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: openhands-integrations-service 5 | labels: 6 | app: openhands-integrations 7 | spec: 8 | ports: 9 | - name: openhands-integrations 10 | port: 3000 11 | protocol: TCP 12 | targetPort: 3000 13 | selector: 14 | app: openhands-integrations 15 | type: ClusterIP -------------------------------------------------------------------------------- /charts/openhands/templates/keycloak-config-script.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: keycloak-config-script 5 | data: 6 | keycloak-config.sh: | 7 | #!/bin/sh 8 | set -e 9 | 10 | keycloak_api_call() { 11 | COMMAND=$1 12 | export RESPONSE=$(eval $COMMAND) 13 | ERROR=$(echo "$RESPONSE" | jq -r 'try if type == "array" then .[0].error else .error end') 14 | if [ -n "$ERROR" ] && [ "null" != "$ERROR" ]; then 15 | echo "Error from Keycloak: $RESPONSE" 16 | exit 1 17 | fi 18 | } 19 | echo "Waiting for Keycloak to be ready..." 20 | until curl --output /dev/null --silent --head --fail $KEYCLOAK_SERVER_URL; do 21 | echo '.' 22 | sleep 5 23 | done 24 | 25 | ACCESS_TOKEN=$(curl -s -X POST "$KEYCLOAK_SERVER_URL/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "grant_type=password" -d "username=tmpadmin" -d "password=$KEYCLOAK_ADMIN_PASSWORD" | jq -r '.access_token') 26 | if [ "$ACCESS_TOKEN" = "null" ]; then 27 | NEW_ACCESS_TOKEN=$(curl -s -X POST "$KEYCLOAK_SERVER_URL/realms/master/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=admin-cli" -d "grant_type=password" -d "username=admin" -d "password=$KEYCLOAK_ADMIN_PASSWORD" | jq -r '.access_token') 28 | if [ "$NEW_ACCESS_TOKEN" = "null" ]; then 29 | echo "Couldn't login using either the \"admin\" or \"tmpadmin\" accounts." 30 | exit 1; 31 | fi 32 | ACCESS_TOKEN=$NEW_ACCESS_TOKEN 33 | fi 34 | 35 | keycloak_api_call "curl -s \"$KEYCLOAK_SERVER_URL/admin/realms/master/users?username=admin&exact=true\" -H \"Authorization: Bearer $ACCESS_TOKEN\"" 36 | if [ "[]" = "$RESPONSE" ]; then 37 | echo "Creating new admin user..." 38 | keycloak_api_call "curl -s -X POST \"$KEYCLOAK_SERVER_URL/admin/realms/master/users\" \ 39 | -H \"Authorization: Bearer $ACCESS_TOKEN\" \ 40 | -H \"Content-Type: application/json\" \ 41 | -d \"{ 42 | \\\"username\\\": \\\"admin\\\", 43 | \\\"enabled\\\": true, 44 | \\\"emailVerified\\\": true, 45 | \\\"firstName\\\": \\\"Keycloak\\\", 46 | \\\"lastName\\\": \\\"Admin\\\", 47 | \\\"email\\\": \\\"admin@all-hands.dev\\\", 48 | \\\"credentials\\\": [{ 49 | \\\"type\\\": \\\"password\\\", 50 | \\\"value\\\": \\\"$KEYCLOAK_ADMIN_PASSWORD\\\", 51 | \\\"temporary\\\": false 52 | }] 53 | }\"" 54 | 55 | keycloak_api_call "curl -s \"$KEYCLOAK_SERVER_URL/admin/realms/master/users?username=admin&exact=true\" -H \"Authorization: Bearer $ACCESS_TOKEN\"" 56 | ADMIN_ID=$(echo "$RESPONSE" | jq -r '.[0].id') 57 | 58 | keycloak_api_call "curl -s \"$KEYCLOAK_SERVER_URL/admin/realms/master/roles/admin\" -H \"Authorization: Bearer $ACCESS_TOKEN\" -H \"Content-Type: application/json\"" 59 | ADMIN_ROLE=$(echo "$RESPONSE" | sed 's/"/\\"/g') 60 | 61 | keycloak_api_call "curl -s \"$KEYCLOAK_SERVER_URL/admin/realms/master/roles/create-realm\" -H \"Authorization: Bearer $ACCESS_TOKEN\" -H \"Content-Type: application/json\"" 62 | CREATE_ROLE=$(echo "$RESPONSE" | sed 's/"/\\"/g') 63 | 64 | keycloak_api_call "curl -s -X POST \"$KEYCLOAK_SERVER_URL/admin/realms/master/users/$ADMIN_ID/role-mappings/realm\" \ 65 | -H \"Authorization: Bearer $ACCESS_TOKEN\" \ 66 | -H \"Content-Type: application/json\" \ 67 | -d \"[$ADMIN_ROLE]\"" 68 | keycloak_api_call "curl -s -X POST \"$KEYCLOAK_SERVER_URL/admin/realms/master/users/$ADMIN_ID/role-mappings/realm\" \ 69 | -H \"Authorization: Bearer $ACCESS_TOKEN\" \ 70 | -H \"Content-Type: application/json\" \ 71 | -d \"[$CREATE_ROLE]\"" 72 | 73 | keycloak_api_call "curl -s -X POST \"$KEYCLOAK_SERVER_URL/realms/master/protocol/openid-connect/token\" -H \"Content-Type: application/x-www-form-urlencoded\" -d \"client_id=admin-cli\" -d \"grant_type=password\" -d \"username=admin\" -d \"password=$KEYCLOAK_ADMIN_PASSWORD\"" 74 | ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token') 75 | 76 | keycloak_api_call "curl -s \"$KEYCLOAK_SERVER_URL/admin/realms/master/users?username=tmpadmin&exact=true\" -H \"Authorization: Bearer $ACCESS_TOKEN\"" 77 | TMPADMIN_ID=$(echo "$RESPONSE" | jq -r '.[0].id') 78 | keycloak_api_call "curl -s -X DELETE \"$KEYCLOAK_SERVER_URL/admin/realms/master/users/$TMPADMIN_ID\" -H \"Authorization: Bearer $ACCESS_TOKEN\"" 79 | 80 | echo "Created new user \"admin\". The password can be found in the \"keycloak-admin\" secret." 81 | else 82 | echo "User already exists: $RESPONSE" 83 | fi 84 | 85 | ERROR_MESSAGE=$(curl -s "$KEYCLOAK_SERVER_URL/admin/realms/$KEYCLOAK_REALM_NAME" -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.error') 86 | echo "Error message: $ERROR_MESSAGE" 87 | export GITHUB_BASE_URL="" 88 | if [ "$GITHUB_PROXY" = "1" ]; then 89 | export GITHUB_BASE_URL="https://{{ .Values.ingress.host }}/github-proxy/test-auth-feat" 90 | fi 91 | if [ "$ERROR_MESSAGE" = "Realm not found." ]; then 92 | echo "Creating allhands realm..." 93 | envsubst '$WEB_HOST,$AUTH_WEB_HOST,$KEYCLOAK_REALM_NAME,$KEYCLOAK_PROVIDER_NAME,$KEYCLOAK_CLIENT_ID,$KEYCLOAK_CLIENT_SECRET,$GITHUB_APP_CLIENT_ID,$GITHUB_APP_CLIENT_SECRET,$GITLAB_APP_CLIENT_ID,$GITLAB_APP_CLIENT_SECRET,$BITBUCKET_APP_CLIENT_ID,$BITBUCKET_APP_CLIENT_SECRET,$GITHUB_BASE_URL,$KEYCLOAK_SMTP_PASSWORD'< /app/allhands-realm-github-provider.json.tmpl > /app/allhands-realm-github-provider.json 94 | keycloak_api_call "curl -s -X POST \"$KEYCLOAK_SERVER_URL/admin/realms\" -H \"Authorization: Bearer $ACCESS_TOKEN\" -H \"Content-Type: application/json\" --data \"@/app/allhands-realm-github-provider.json\"" 95 | echo "Created allhands realm." 96 | fi -------------------------------------------------------------------------------- /charts/openhands/templates/maintenance-tasks-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.maintenanceTasks.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ .Release.Name }}-maintenance-tasks 6 | labels: 7 | app: {{ .Release.Name }}-maintenance-tasks 8 | spec: 9 | schedule: {{ .Values.maintenanceTasks.schedule | quote }} 10 | concurrencyPolicy: {{ .Values.maintenanceTasks.concurrencyPolicy }} 11 | failedJobsHistoryLimit: {{ .Values.maintenanceTasks.failedJobsHistoryLimit }} 12 | successfulJobsHistoryLimit: {{ .Values.maintenanceTasks.successfulJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | backoffLimit: {{ .Values.maintenanceTasks.backoffLimit }} 16 | ttlSecondsAfterFinished: 3600 17 | template: 18 | metadata: 19 | name: maintenance-tasks 20 | labels: 21 | app: {{ .Release.Name }}-maintenance-tasks 22 | spec: 23 | {{- with .Values.securityContext }} 24 | securityContext: 25 | {{- toYaml . | nindent 12 }} 26 | {{- end }} 27 | serviceAccountName: {{ .Values.serviceAccount.name }} 28 | restartPolicy: Never 29 | {{- with .Values.imagePullSecrets }} 30 | imagePullSecrets: 31 | {{- toYaml . | nindent 12 }} 32 | {{- end }} 33 | containers: 34 | - name: maintenance-tasks 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | command: ["/bin/sh", "-c"] 37 | workingDir: /app 38 | args: 39 | - | 40 | echo "Running maintenance tasks..." 41 | python -m run_maintenance_tasks 42 | env: 43 | {{- include "openhands.env" . | nindent 16 }} 44 | resources: 45 | {{- toYaml .Values.maintenanceTasks.resources | nindent 16 }} 46 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/mcp-events-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: openhands-mcp 5 | labels: 6 | app: openhands-mcp 7 | spec: 8 | replicas: {{ .Values.mcpEvents.deployment.replicas | default 2 }} 9 | selector: 10 | matchLabels: 11 | app: openhands-mcp 12 | template: 13 | metadata: 14 | labels: 15 | app: openhands-mcp 16 | annotations: 17 | {{- if .Values.allowedUsers }} 18 | checksum/user-waitlist: {{ include (print $.Template.BasePath "/user-waitlist-configmap.yaml") . | sha256sum }} 19 | {{- end }} 20 | spec: 21 | terminationGracePeriodSeconds: 60 22 | serviceAccountName: {{ .Values.serviceAccount.name }} 23 | {{- with .Values.securityContext }} 24 | securityContext: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | volumes: 32 | {{- if .Values.allowedUsers }} 33 | - name: user-waitlist 34 | configMap: 35 | name: user-waitlist 36 | {{- end }} 37 | containers: 38 | - name: openhands-mcp 39 | imagePullPolicy: Always 40 | image: '{{.Values.image.repository}}:{{.Values.image.tag | default .Chart.AppVersion }}' 41 | command: ["/bin/sh"] 42 | args: 43 | - "-c" 44 | - "exec {{- if .Values.datadog.enabled }} ddtrace-run {{- end }} uvicorn saas_server:app --host 0.0.0.0 --port 3000 --workers {{ .Values.mcpEvents.uvicorn.workers | default 2 }}" 45 | ports: 46 | - containerPort: 3000 47 | resources: 48 | {{- toYaml .Values.deployment.resources | nindent 12 }} 49 | startupProbe: 50 | httpGet: 51 | path: /health 52 | port: 3000 53 | failureThreshold: 30 54 | periodSeconds: 10 55 | livenessProbe: 56 | httpGet: 57 | path: /health 58 | port: 3000 59 | initialDelaySeconds: 10 60 | periodSeconds: 10 61 | failureThreshold: 3 62 | readinessProbe: 63 | httpGet: 64 | path: /health 65 | port: 3000 66 | initialDelaySeconds: 5 67 | periodSeconds: 10 68 | failureThreshold: 3 69 | volumeMounts: 70 | {{- if .Values.allowedUsers }} 71 | - name: user-waitlist 72 | mountPath: /app/user-waitlist 73 | {{- end }} 74 | env: 75 | {{- include "openhands.env" . | nindent 8 }} -------------------------------------------------------------------------------- /charts/openhands/templates/mcp-events-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: openhands-mcp-service 5 | labels: 6 | app: openhands-mcp 7 | spec: 8 | ports: 9 | - name: openhands-mcp 10 | port: 3000 11 | protocol: TCP 12 | targetPort: 3000 13 | selector: 14 | app: openhands-mcp 15 | type: ClusterIP -------------------------------------------------------------------------------- /charts/openhands/templates/migrate-db.job.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.migrationJob.enabled }} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: migrate-db 6 | annotations: 7 | {{- if not .Values.externalDatabase.enabled }} 8 | "helm.sh/hook": "post-install,post-upgrade" 9 | "helm.sh/hook-weight": "-5" 10 | "helm.sh/hook-delete-policy": "before-hook-creation" 11 | {{- end }} 12 | spec: 13 | ttlSecondsAfterFinished: 3600 14 | backoffLimit: 0 15 | activeDeadlineSeconds: 600 16 | template: 17 | metadata: 18 | name: migrate-db 19 | spec: 20 | serviceAccountName: {{ .Values.serviceAccount.name }} 21 | restartPolicy: Never 22 | {{- with .Values.securityContext }} 23 | securityContext: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | {{- if .Values.migrationJob.initContainer.enabled }} 31 | {{- if .Values.postgresql.enabled }} 32 | initContainers: 33 | - name: wait-for-postgres 34 | image: "bitnamilegacy/postgresql:latest" 35 | command: ['sh', '-c'] 36 | args: 37 | - | 38 | DB_HOST="oh-main-postgresql" 39 | echo "Waiting for PostgreSQL at $DB_HOST to be ready..." 40 | until PGPASSWORD=$DB_PASS psql -h $DB_HOST -p 5432 -U postgres -c '\q' > /dev/null 2>&1; do 41 | echo "PostgreSQL is unavailable - sleeping for 2 seconds" 42 | sleep 2 43 | done 44 | echo "PostgreSQL is up and running!" 45 | env: 46 | {{- include "openhands.env" . | nindent 12 }} 47 | {{- else if .Values.externalDatabase.enabled }} 48 | initContainers: 49 | - name: check-db-exists 50 | image: "bitnamilegacy/postgresql:latest" 51 | command: ['sh', '-c'] 52 | args: 53 | - | 54 | echo "Waiting for RDS PostgreSQL at $DB_HOST to be ready..." 55 | export PGPASSWORD=$DB_ADMIN_PASSWORD 56 | until psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -c '\q' > /dev/null 2>&1; do 57 | echo "PostgreSQL is unavailable - sleeping for 2 seconds" 58 | sleep 2 59 | done 60 | echo "PostgreSQL is up and running!" 61 | 62 | echo "Checking if the keycloak DB \"$KEYCLOAK_DB_NAME\" and keycloak user \"$KEYCLOAK_DB_USER\" exist..." 63 | # Create the keylcloak database if it doesn't exist 64 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$KEYCLOAK_DB_NAME'" | grep -q 1 || \ 65 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -c "CREATE DATABASE $KEYCLOAK_DB_NAME;" 66 | 67 | # Create the keycloak user if it doesn't exist 68 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $KEYCLOAK_DB_NAME -tc "SELECT 1 FROM pg_roles WHERE rolname='$KEYCLOAK_DB_USER'" | grep -q 1 || \ 69 | (psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $KEYCLOAK_DB_NAME -c "CREATE USER $KEYCLOAK_DB_USER WITH PASSWORD '$KEYCLOAK_DB_PASS'; GRANT ALL PRIVILEGES ON DATABASE $KEYCLOAK_DB_NAME TO $KEYCLOAK_DB_USER;") 70 | 71 | echo "Checking if the DB \"$DB_NAME\" and user \"$DB_USER\" exist..." 72 | # Create the database if it doesn't exist 73 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || \ 74 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -c "CREATE DATABASE $DB_NAME;" 75 | 76 | # Create the user if it doesn't exist 77 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $DB_NAME -tc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || \ 78 | (psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $DB_NAME -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS'; GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;") 79 | env: 80 | - name: DB_ADMIN_PASSWORD 81 | valueFrom: 82 | secretKeyRef: 83 | name: {{ .Values.externalDatabase.existingSecret }} 84 | key: admin_password 85 | - name: KEYCLOAK_DB_USER 86 | valueFrom: 87 | secretKeyRef: 88 | name: {{ .Values.keycloak.externalDatabase.existingSecret }} 89 | key: db-user 90 | - name: KEYCLOAK_DB_PASS 91 | valueFrom: 92 | secretKeyRef: 93 | name: {{ .Values.keycloak.externalDatabase.existingSecret }} 94 | key: db-password 95 | - name: KEYCLOAK_DB_NAME 96 | valueFrom: 97 | secretKeyRef: 98 | name: {{ .Values.keycloak.externalDatabase.existingSecret }} 99 | key: db-name 100 | - name: KEYCLOAK_DB_HOST 101 | valueFrom: 102 | secretKeyRef: 103 | name: {{ .Values.keycloak.externalDatabase.existingSecret }} 104 | key: db-host 105 | - name: KEYCLOAK_DB_PORT 106 | valueFrom: 107 | secretKeyRef: 108 | name: {{ .Values.keycloak.externalDatabase.existingSecret }} 109 | key: db-port 110 | {{- include "openhands.env" . | nindent 12 }} 111 | {{- end }} 112 | {{- end }} 113 | containers: 114 | - name: migrate-db 115 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 116 | command: ["/bin/sh", "-c"] 117 | workingDir: /app 118 | args: 119 | - | 120 | echo "Running migrations..." 121 | alembic upgrade head 122 | env: 123 | {{- include "openhands.env" . | nindent 12 }} 124 | {{- end }} 125 | -------------------------------------------------------------------------------- /charts/openhands/templates/monitoring.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.gcpMonitoring.enabled }} 2 | apiVersion: monitoring.googleapis.com/v1 3 | kind: PodMonitoring 4 | metadata: 5 | name: openhands-monitoring 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: openhands 10 | endpoints: 11 | - port: 3000 12 | interval: 30s 13 | # We currently need the trailing slash 14 | path: /internal/metrics/ 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /charts/openhands/templates/proactive-convo-clean-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.proactiveConvoClean.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ .Release.Name }}-proactive-convo-clean 6 | labels: 7 | app: {{ .Release.Name }}-proactive-convo-clean 8 | spec: 9 | schedule: {{ .Values.proactiveConvoClean.schedule | quote }} 10 | concurrencyPolicy: {{ .Values.proactiveConvoClean.concurrencyPolicy }} 11 | failedJobsHistoryLimit: {{ .Values.proactiveConvoClean.failedJobsHistoryLimit }} 12 | successfulJobsHistoryLimit: {{ .Values.proactiveConvoClean.successfulJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | backoffLimit: {{ .Values.proactiveConvoClean.backoffLimit }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ .Release.Name }}-proactive-convo-clean 20 | spec: 21 | {{- with .Values.securityContext }} 22 | securityContext: 23 | {{- toYaml . | nindent 12 }} 24 | {{- end }} 25 | serviceAccountName: {{ .Values.serviceAccount.name }} 26 | restartPolicy: OnFailure 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 12 }} 30 | {{- end }} 31 | containers: 32 | - name: {{ .Release.Name }}-proactive-convo-clean 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} 35 | command: ["python", "-m", "sync.clean_proactive_convo_table"] 36 | resources: 37 | {{- toYaml .Values.proactiveConvoClean.resources | nindent 16 }} 38 | env: 39 | {{- include "openhands.env" . | nindent 16 }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /charts/openhands/templates/resend-sync-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.resendSync.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ .Release.Name }}-resend-sync 6 | labels: 7 | app: {{ .Release.Name }}-resend-sync 8 | spec: 9 | schedule: {{ .Values.resendSync.schedule | quote }} 10 | concurrencyPolicy: {{ .Values.resendSync.concurrencyPolicy }} 11 | failedJobsHistoryLimit: {{ .Values.resendSync.failedJobsHistoryLimit }} 12 | successfulJobsHistoryLimit: {{ .Values.resendSync.successfulJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | backoffLimit: {{ .Values.resendSync.backoffLimit }} 16 | template: 17 | metadata: 18 | labels: 19 | app: {{ .Release.Name }}-resend-sync 20 | spec: 21 | {{- with .Values.securityContext }} 22 | securityContext: 23 | {{- toYaml . | nindent 12 }} 24 | {{- end }} 25 | serviceAccountName: {{ .Values.serviceAccount.name }} 26 | restartPolicy: OnFailure 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 12 }} 30 | {{- end }} 31 | containers: 32 | - name: {{ .Release.Name }}-resend-sync 33 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 34 | imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} 35 | command: ["python", "-m", "sync.resend_keycloak"] 36 | resources: 37 | {{- toYaml .Values.resendSync.resources | nindent 16 }} 38 | env: 39 | - name: RESEND_AUDIENCE_ID 40 | value: {{ .Values.resendSync.audienceId | quote }} 41 | - name: KEYCLOAK_REALM 42 | valueFrom: 43 | secretKeyRef: 44 | name: keycloak-realm 45 | key: realm-name 46 | - name: BATCH_SIZE 47 | value: {{ .Values.resendSync.batchSize | quote }} 48 | - name: MAX_RETRIES 49 | value: {{ .Values.resendSync.maxRetries | quote }} 50 | - name: INITIAL_BACKOFF_SECONDS 51 | value: {{ .Values.resendSync.initialBackoffSeconds | quote }} 52 | - name: MAX_BACKOFF_SECONDS 53 | value: {{ .Values.resendSync.maxBackoffSeconds | quote }} 54 | - name: BACKOFF_FACTOR 55 | value: {{ .Values.resendSync.backoffFactor | quote }} 56 | - name: RATE_LIMIT 57 | value: {{ .Values.resendSync.rateLimit | quote }} 58 | - name: RESEND_API_KEY 59 | valueFrom: 60 | secretKeyRef: 61 | name: resend-api-key 62 | key: resend-api-key 63 | {{- if .Values.resendSync.fromEmail }} 64 | - name: RESEND_FROM_EMAIL 65 | value: {{ .Values.resendSync.fromEmail | quote }} 66 | {{- end }} 67 | {{- include "openhands.env" . | nindent 16 }} 68 | {{- end }} 69 | -------------------------------------------------------------------------------- /charts/openhands/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ .Values.serviceAccount.name }} 6 | annotations: 7 | {{- if .Values.serviceAccount.annotations }} 8 | {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} 9 | {{- end }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /charts/openhands/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: openhands-service 5 | labels: 6 | app: openhands 7 | spec: 8 | ports: 9 | - name: openhands 10 | port: 3000 11 | protocol: TCP 12 | targetPort: 3000 13 | selector: 14 | app: openhands 15 | type: ClusterIP 16 | {{- if index .Values "runtime-api" "enabled" }} 17 | --- 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: oh-main-runtime-api 22 | spec: 23 | type: ClusterIP 24 | ports: 25 | - port: 5000 26 | targetPort: http 27 | protocol: TCP 28 | name: http 29 | selector: 30 | app.kubernetes.io/name: runtime-api 31 | app.kubernetes.io/instance: {{ .Release.Name }} 32 | {{- end }} 33 | {{- if .Values.postgresql.enabled }} 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: oh-main-postgresql 39 | spec: 40 | type: ClusterIP 41 | sessionAffinity: None 42 | ports: 43 | - name: tcp-postgresql 44 | port: 5432 45 | targetPort: tcp-postgresql 46 | nodePort: null 47 | selector: 48 | app.kubernetes.io/instance: {{ .Release.Name }} 49 | app.kubernetes.io/name: postgresql 50 | app.kubernetes.io/component: primary 51 | {{- end }} 52 | {{- if and .Values.redis.enabled .Values.langfuse.enabled }} 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | name: oh-main-redis 58 | spec: 59 | type: ClusterIP 60 | internalTrafficPolicy: Cluster 61 | sessionAffinity: None 62 | ports: 63 | - name: tcp-redis 64 | port: 6379 65 | targetPort: redis 66 | nodePort: null 67 | selector: 68 | app.kubernetes.io/instance: {{ .Release.Name }} 69 | app.kubernetes.io/name: redis 70 | app.kubernetes.io/component: master 71 | {{- end }} 72 | {{- if index .Values "litellm-helm" "enabled" }} 73 | --- 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: oh-main-lite-llm 78 | spec: 79 | type: ClusterIP 80 | internalTrafficPolicy: Cluster 81 | sessionAffinity: None 82 | ports: 83 | - name: http 84 | port: 4000 85 | targetPort: http 86 | selector: 87 | app.kubernetes.io/instance: {{ .Release.Name }} 88 | app.kubernetes.io/name: litellm 89 | {{- end }} 90 | {{- if .Values.clickhouse.enabled }} 91 | apiVersion: v1 92 | kind: Service 93 | metadata: 94 | name: oh-main-clickhouse 95 | namespace: openhands 96 | labels: 97 | app: clickhouse 98 | spec: 99 | type: ClusterIP 100 | ports: 101 | - name: http 102 | port: 8123 103 | protocol: TCP 104 | targetPort: 8123 105 | - name: tcp 106 | port: 9000 107 | protocol: TCP 108 | targetPort: 9000 109 | - name: tcp-mysql 110 | port: 9004 111 | protocol: TCP 112 | targetPort: 9004 113 | - name: tcp-postgresql 114 | port: 9005 115 | protocol: TCP 116 | targetPort: 9005 117 | - name: http-intersrv 118 | port: 9009 119 | protocol: TCP 120 | targetPort: 9009 121 | selector: 122 | app.kubernetes.io/instance: {{ .Release.Name }} 123 | app.kubernetes.io/name: clickhouse 124 | {{- end }} 125 | {{- if .Values.langfuse.enabled }} 126 | --- 127 | apiVersion: v1 128 | kind: Service 129 | metadata: 130 | name: oh-main-langfuse 131 | spec: 132 | type: ClusterIP 133 | ports: 134 | - name: http 135 | port: 3000 136 | targetPort: http 137 | nodePort: null 138 | selector: 139 | app.kubernetes.io/instance: {{ .Release.Name }} 140 | app.kubernetes.io/name: langfuse 141 | {{- end }} 142 | -------------------------------------------------------------------------------- /charts/openhands/templates/tlsstore.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.tlsStore.enabled }} 2 | apiVersion: traefik.io/v1alpha1 3 | kind: TLSStore 4 | metadata: 5 | name: {{ .Values.tlsStore.name }} 6 | spec: 7 | defaultCertificate: 8 | secretName: {{ .Values.tlsStore.secretName }} 9 | {{- end }} -------------------------------------------------------------------------------- /charts/openhands/templates/user-waitlist-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.allowedUsers }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: user-waitlist 6 | data: 7 | user-waitlist.txt: | 8 | {{- range .Values.allowedUsers }} 9 | {{ . }} 10 | {{- end }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /charts/openhands/values.runtime-example.yaml: -------------------------------------------------------------------------------- 1 | # Example values file for a runtime environment with wildcard certificate 2 | ingress: 3 | enabled: true 4 | host: "example.prod-runtime.all-hands.dev" 5 | class: traefik 6 | annotations: 7 | # No need for cert-manager annotation as we're using the wildcard certificate 8 | # that's already created and managed by the infrastructure 9 | 10 | # Enable the wildcard certificate 11 | certificate: 12 | enabled: true 13 | name: runtime-wildcard 14 | secretName: runtime-wildcard-tls 15 | issuer: google-clouddns 16 | domains: 17 | - "*.prod-runtime.all-hands.dev" 18 | commonName: "*.prod-runtime.all-hands.dev" 19 | 20 | # Enable the TLSStore for Traefik to use the wildcard certificate as default 21 | tlsStore: 22 | enabled: true 23 | name: default 24 | secretName: runtime-wildcard-tls 25 | 26 | # No need to enable TLS separately as it's handled by the certificate 27 | tls: 28 | enabled: false -------------------------------------------------------------------------------- /charts/openhands/values.yaml: -------------------------------------------------------------------------------- 1 | allowedUsers: null 2 | 3 | autoscaling: 4 | enabled: false 5 | minReplicas: 1 6 | maxReplicas: 5 7 | targetCPUUtilizationPercentage: 80 8 | targetMemoryUtilizationPercentage: 80 9 | 10 | appConfig: 11 | OPENHANDS_CONFIG_CLS: "server.config.SaaSServerConfig" 12 | OPENHANDS_CONVERSATION_VALIDATOR_CLS: "storage.saas_conversation_validator.SaasConversationValidator" 13 | OPENHANDS_GITHUB_SERVICE_CLS: "integrations.github.github_service.SaaSGitHubService" 14 | OPENHANDS_GITLAB_SERVICE_CLS: "integrations.gitlab.gitlab_service.SaaSGitLabService" 15 | OPENHANDS_BITBUCKET_SERVICE_CLS: "integrations.bitbucket.bitbucket_service.SaaSBitBucketService" 16 | OPENHANDS_MCP_CONFIG_CLS: "server.mcp.mcp_config.SaaSOpenHandsMCPConfig" 17 | OPENHANDS_EXPERIMENT_MANAGER_CLS: "experiments.experiment_manager.SaaSExperimentManager" 18 | POSTHOG_CLIENT_KEY: "1234abcd" 19 | 20 | commonRoomSync: 21 | enabled: false 22 | schedule: "0 3 * * *" # Daily at 3 AM 23 | concurrencyPolicy: Forbid 24 | failedJobsHistoryLimit: 3 25 | successfulJobsHistoryLimit: 3 26 | backoffLimit: 3 27 | batchSize: "100" 28 | maxRetries: "3" 29 | initialBackoffSeconds: "1" 30 | maxBackoffSeconds: "60" 31 | backoffFactor: "2" 32 | rateLimit: "2" 33 | keycloakBatchSize: "20" 34 | resources: 35 | requests: 36 | memory: 256Mi 37 | cpu: 100m 38 | limits: 39 | memory: 512Mi 40 | cpu: 200m 41 | 42 | datadog: 43 | enabled: false 44 | env: "dev" # Default environment, can be overridden 45 | service: "openhands" 46 | agentHost: "datadog-agent" 47 | 48 | debuggingRoutes: 49 | enabled: false 50 | 51 | deployment: 52 | replicas: 1 53 | resources: 54 | requests: 55 | memory: 3Gi 56 | cpu: 1 57 | limits: 58 | memory: 3Gi 59 | 60 | # Security context settings for all pods 61 | securityContext: 62 | runAsUser: 42420 63 | runAsGroup: 42420 64 | fsGroup: 42420 # optional, if the pod needs to write to mounted volumes 65 | fsGroupChangePolicy: Always 66 | runAsNonRoot: true 67 | 68 | env: 69 | DISABLE_WAITLIST: "true" 70 | SANDBOX_REMOTE_RUNTIME_API_TIMEOUT: "60" 71 | RUNTIME_URL_PATTERN: "https://{runtime_id}.runtime.chuck-test.aws.all-hands.dev" 72 | # replace prod/claude-3-7-sonnet-20250219 with your LLM model and uncomment or override this variable 73 | # LITELLM_DEFAULT_MODEL: "litellm_proxy/prod/claude-3-7-sonnet-20250219" 74 | 75 | filestore: 76 | ephemeral: false 77 | bucket: openhands-sessions 78 | 79 | github: 80 | enabled: false 81 | 82 | enrichUserInteractionData: 83 | enabled: false 84 | schedule: "*/10 * * * *" # Run every 10 minutes 85 | concurrencyPolicy: Forbid 86 | failedJobsHistoryLimit: 3 87 | successfulJobsHistoryLimit: 3 88 | backoffLimit: 3 89 | resources: 90 | requests: 91 | memory: "512Mi" 92 | cpu: "200m" 93 | limits: 94 | memory: "512Mi" 95 | cpu: "200m" 96 | 97 | githubProxy: 98 | enabled: false 99 | endpointsEnabled: false 100 | 101 | gitlab: 102 | enabled: false 103 | 104 | bitbucket: 105 | enabled: false 106 | auth: 107 | existingSecret: bitbucket-app 108 | 109 | gitlabWebhookInstallation: 110 | enabled: false 111 | schedule: "* * * * *" 112 | concurrencyPolicy: Forbid 113 | failedJobsHistoryLimit: 3 114 | successfulJobsHistoryLimit: 3 115 | backoffLimit: 3 116 | resources: 117 | requests: 118 | memory: 256Mi 119 | cpu: 100m 120 | limits: 121 | memory: 512Mi 122 | cpu: 200m 123 | 124 | image: 125 | repository: ghcr.io/all-hands-ai/enterprise-server 126 | tag: sha-bd51673 127 | 128 | ingress: 129 | enabled: false 130 | # REQUIRED: Update to a hostname in a DNS domain you own 131 | # host: "app.example.com" 132 | class: traefik 133 | # Legacy annotations field - kept for backward compatibility 134 | annotations: {} 135 | # UPDATE: if you use cert-manager, enter your clusterIssuer may not match. 136 | # cert-manager.io/cluster-issuer: letsencrypt-production 137 | 138 | # Separate annotations for each ingress 139 | root: 140 | annotations: {} 141 | # Example: Add specific annotations for the root ingress 142 | # nginx.ingress.kubernetes.io/proxy-body-size: "100m" 143 | 144 | integrations: 145 | annotations: {} 146 | # Example: Add specific annotations for integrations ingress 147 | # nginx.ingress.kubernetes.io/rate-limit: "100" 148 | 149 | mcp: 150 | annotations: {} 151 | # Example: Add specific annotations for MCP ingress 152 | # nginx.ingress.kubernetes.io/auth-type: basic 153 | 154 | integrationEvents: 155 | deployment: 156 | replicas: 2 157 | resources: 158 | requests: 159 | memory: 1.5Gi 160 | cpu: 1000m 161 | limits: 162 | memory: 1.5Gi 163 | cpu: 1000m 164 | uvicorn: 165 | workers: 2 166 | 167 | litellm: 168 | enabled: true 169 | # NOTE: to set the LLM model, set the LITELLM_DEFAULT_MODEL variable in the env section above. 170 | # REQUIRED: Update to match the hostname set in the litellm-helm ingress section 171 | url: "https://llm-proxy.example.com" 172 | # REQUIRED: Update this AFTER you initially install the chart and login to litellm to create the team. 173 | # teamId: "" 174 | auth: 175 | existingSecret: lite-llm-api-key 176 | 177 | mcpEvents: 178 | deployment: 179 | replicas: 2 180 | uvicorn: 181 | workers: 2 182 | 183 | migrationJob: 184 | enabled: true 185 | initContainer: 186 | enabled: true 187 | 188 | gcpMonitoring: 189 | enabled: false 190 | 191 | proactiveConvoClean: 192 | enabled: false 193 | schedule: "0 2 * * *" # Daily at 2 AM 194 | concurrencyPolicy: Forbid 195 | failedJobsHistoryLimit: 3 196 | successfulJobsHistoryLimit: 1 197 | backoffLimit: 3 198 | resources: 199 | requests: 200 | memory: 256Mi 201 | cpu: 100m 202 | limits: 203 | memory: 512Mi 204 | cpu: 200m 205 | 206 | resendSync: 207 | enabled: false 208 | schedule: "0 1 * * *" # Daily at 1 AM 209 | concurrencyPolicy: Forbid 210 | failedJobsHistoryLimit: 3 211 | successfulJobsHistoryLimit: 1 212 | backoffLimit: 3 213 | audienceId: "" 214 | batchSize: "100" 215 | maxRetries: "3" 216 | initialBackoffSeconds: "1" 217 | maxBackoffSeconds: "60" 218 | backoffFactor: "2" 219 | rateLimit: "10" 220 | resources: 221 | requests: 222 | memory: 256Mi 223 | cpu: 100m 224 | limits: 225 | memory: 512Mi 226 | cpu: 200m 227 | 228 | runtime: 229 | image: 230 | repository: ghcr.io/all-hands-ai/runtime 231 | tag: 135d3aea09780748db552d0c73f1f1c795191ed8-nikolaik 232 | runAsRoot: true 233 | 234 | sandbox: 235 | # REQUIRED: Update so this matches what you have set in the runtime api ingress 236 | # apiHostname: https://runtime-api.example.com 237 | apiHostname: "https://dummy-runtime-api.example.com" 238 | 239 | serviceAccount: 240 | create: true 241 | name: openhands-sa 242 | 243 | sessions: 244 | existingSecret: jwt-secret 245 | 246 | slack: 247 | enabled: false 248 | 249 | stripe: 250 | enabled: false 251 | requirePayment: false 252 | auth: 253 | existingSecret: stripe-secret 254 | 255 | tavily: 256 | enabled: false 257 | auth: 258 | existingSecret: tavily-api-key 259 | 260 | tls: 261 | enabled: false 262 | 263 | certificate: 264 | enabled: false 265 | name: runtime-wildcard 266 | secretName: runtime-wildcard-tls 267 | issuer: google-clouddns 268 | domains: [] 269 | # Example domains configuration: 270 | # domains: 271 | # - "*.prod-runtime.all-hands.dev" 272 | # commonName: "*.prod-runtime.all-hands.dev" 273 | 274 | tlsStore: 275 | enabled: false 276 | name: default 277 | secretName: runtime-wildcard-tls 278 | 279 | uvicorn: 280 | workers: 3 281 | 282 | ## Chart dependency configurations 283 | 284 | clickhouse: 285 | # Enable this if you enable langfuse, as it will use clickhouse for storage 286 | enabled: false 287 | shards: 2 288 | replicaCount: 1 289 | keeper: 290 | enabled: true 291 | auth: 292 | username: default 293 | existingSecret: clickhouse-password 294 | existingSecretKey: password 295 | image: 296 | repository: bitnamilegacy/clickhouse 297 | 298 | keycloak: 299 | enabled: false 300 | url: "http://keycloak" 301 | fullnameOverride: keycloak 302 | replicaCount: 1 303 | production: true 304 | proxyHeaders: forwarded 305 | ingress: 306 | enabled: false 307 | # REQUIRED: Update to a hostname in a DNS domain you own 308 | # hostname: auth.app.example.com 309 | servicePort: 80 310 | tls: true 311 | annotations: {} 312 | # UPDATE: if you use cert-manager, enter your clusterIssuer may not match. 313 | # cert-manager.io/cluster-issuer: letsencrypt-production 314 | ingressClassName: traefik 315 | auth: 316 | adminUser: tmpadmin 317 | existingSecret: keycloak-admin 318 | passwordSecretKey: admin-password 319 | service: 320 | type: ClusterIP 321 | serviceAccount: 322 | name: "keycloak-sa" 323 | postgresql: 324 | enabled: false 325 | externalDatabase: 326 | host: oh-main-postgresql 327 | existingSecret: postgres-password 328 | existingSecretUserKey: username 329 | existingSecretPasswordKey: password 330 | extraEnvVars: 331 | - name: KC_FEATURES 332 | value: token-exchange,admin-fine-grained-authz 333 | image: 334 | repository: bitnamilegacy/keycloak 335 | 336 | langfuse: 337 | # Enable this if you want to use langfuse for tracing 338 | enabled: false 339 | langfuse: 340 | salt: 341 | secretKeyRef: 342 | name: langfuse-salt 343 | key: salt 344 | nextauth: 345 | secret: 346 | secretKeyRef: 347 | name: langfuse-nextauth 348 | key: nextauth-secret 349 | clickhouse: 350 | deploy: false 351 | host: oh-main-clickhouse 352 | auth: 353 | username: default 354 | existingSecret: clickhouse-password 355 | existingSecretKey: password 356 | postgresql: 357 | deploy: false 358 | host: oh-main-postgresql 359 | auth: 360 | username: postgres 361 | existingSecret: postgres-password 362 | existingSecretKey: password 363 | redis: 364 | deploy: false 365 | host: oh-main-redis 366 | auth: 367 | existingSecret: redis 368 | existingSecretPasswordKey: redis-password 369 | 370 | litellm-helm: 371 | # Set this to false if you are using your own litellm instance 372 | # NOTE: We recommend using the provided LiteLLM instance (enabled: true) for simplicity 373 | # and because it is the most extensively tested scenario. Our automation uses an admin key 374 | # to do user management for the LiteLLM instance. 375 | enabled: false 376 | masterkeySecretName: lite-llm-api-key 377 | masterkeySecretKey: lite-llm-api-key 378 | environmentSecrets: [litellm-env-secrets] 379 | # Uncomment this if you want to use langfuse for tracing (and enable langfuse below) 380 | # envVars: 381 | # LANGFUSE_HOST: "http://oh-main-langfuse" 382 | db: 383 | deployStandalone: false 384 | useExisting: true 385 | database: litellm 386 | endpoint: "oh-main-postgresql" 387 | secret: 388 | name: postgres-password 389 | usernameKey: username 390 | passwordKey: password 391 | ingress: 392 | enabled: false 393 | className: traefik 394 | annotations: 395 | cert-manager.io/cluster-issuer: letsencrypt-production 396 | hosts: 397 | # REQUIRED: Update to a hostname in a DNS domain you own 398 | # - host: llm-proxy.example.com 399 | # paths: 400 | # - path: / 401 | # pathType: Prefix 402 | tls: 403 | - secretName: llm-proxy-tls 404 | # REQUIRED: Update to match the hostname you set above 405 | hosts: 406 | - llm-proxy.example.com 407 | proxy_config: 408 | environment_variables: 409 | { 410 | "OR_APP_NAME": "OpenHands", 411 | "OR_SITE_URL": "https://docs.all-hands.dev", 412 | } 413 | model_list: 414 | # UPDATE to use models that you have access to and have an API key stored in the litellm-env-secrets secret 415 | # - model_name: "prod/claude-3-5-sonnet-20241022" 416 | # litellm_params: 417 | # model: "anthropic/claude-3-5-sonnet-20241022" 418 | # api_key: os.environ/ANTHROPIC_API_KEY 419 | # - model_name: "prod/claude-3-7-sonnet-20250219" 420 | # litellm_params: 421 | # model: "anthropic/claude-3-7-sonnet-20250219" 422 | # api_key: os.environ/ANTHROPIC_API_KEY 423 | litellm_settings: 424 | num_retries: 3 425 | request_timeout: 600 426 | # Uncomment this if you want to use langfuse for tracing (and enable langfuse below) 427 | # success_callback: ["langfuse"] 428 | # failure_callback: ["langfuse"] 429 | # langfuse_default_tags: ["cache_hit", "cache_key", "proxy_base_url", "user_api_key_alias", "user_api_key_user_id", "user_api_key_team_alias"] 430 | context_window_fallbacks: [] 431 | allowed_fails: 3 432 | 433 | minio: 434 | replicas: 1 435 | mode: standalone 436 | persistence: 437 | enabled: false 438 | buckets: 439 | - name: openhands-sessions 440 | policy: none 441 | purge: true 442 | svcaccts: 443 | - accessKey: "default" 444 | secretKey: "notasecret" 445 | user: root 446 | rootUser: "root" 447 | resources: 448 | requests: 449 | memory: 512Mi 450 | cpu: 100m 451 | limits: 452 | memory: 512Mi 453 | cpu: 100m 454 | 455 | postgresql: 456 | enabled: true 457 | auth: 458 | username: postgres 459 | existingSecret: postgres-password 460 | primary: 461 | initdb: 462 | scriptsConfigMap: oh-psql-scripts-configmap 463 | persistence: 464 | enabled: false 465 | # Add the following if using Karpenter with persistence 466 | # podAnnotations: 467 | # karpenter.sh/do-not-evict: "true" 468 | service: 469 | ports: 470 | postgresql: 5432 471 | image: 472 | repository: bitnamilegacy/postgresql 473 | 474 | externalDatabase: 475 | enabled: false 476 | existingSecret: postgres-password 477 | 478 | redis: 479 | enabled: true 480 | architecture: standalone 481 | auth: 482 | enabled: true 483 | existingSecret: "redis" 484 | master: 485 | persistence: 486 | enabled: false 487 | replica: 488 | replicaCount: 0 489 | resources: 490 | requests: 491 | memory: 512Mi 492 | cpu: 100m 493 | limits: 494 | memory: 512Mi 495 | cpu: 100m 496 | image: 497 | repository: bitnamilegacy/redis 498 | 499 | maintenanceTasks: 500 | enabled: true 501 | schedule: "0 6 * * *" # Run daily at 1AM Eastern Time (6AM UTC) 502 | concurrencyPolicy: Forbid 503 | failedJobsHistoryLimit: 3 504 | successfulJobsHistoryLimit: 3 505 | backoffLimit: 3 506 | resources: 507 | requests: 508 | memory: "512Mi" 509 | cpu: "200m" 510 | limits: 511 | memory: "1Gi" 512 | cpu: "500m" 513 | 514 | runtime-api: 515 | enabled: true 516 | replicaCount: 1 517 | monitoring: 518 | enabled: false 519 | runtimeInSameCluster: true 520 | image: 521 | tag: sha-f3c679d 522 | imagePullSecrets: [] 523 | securityContext: 524 | runAsUser: 1000 525 | runAsGroup: 1000 526 | fsGroup: 1000 527 | fsGroupChangePolicy: Always 528 | runAsNonRoot: true 529 | resources: 530 | requests: 531 | cpu: 500m 532 | memory: 1.5Gi 533 | limits: 534 | cpu: null 535 | memory: 1.5Gi 536 | warmRuntimes: 537 | enabled: true 538 | count: 1 539 | configs: 540 | - name: default 541 | image: "ghcr.io/all-hands-ai/runtime:135d3aea09780748db552d0c73f1f1c795191ed8-nikolaik" 542 | working_dir: "/openhands/code/" 543 | environment: {} 544 | command: 545 | - /openhands/micromamba/bin/micromamba 546 | - run 547 | - -n 548 | - openhands 549 | - poetry 550 | - run 551 | - python 552 | - -u 553 | - -m 554 | - openhands.runtime.action_execution_server 555 | - "60000" 556 | - --working-dir 557 | - /workspace 558 | - --plugins 559 | - agent_skills 560 | - jupyter 561 | - vscode 562 | - --username 563 | - root 564 | - --user-id 565 | - "0" 566 | postgresql: 567 | postMigrate: true 568 | auth: 569 | username: postgres 570 | existingSecret: postgres-password 571 | externalDatabase: 572 | enabled: false 573 | existingSecret: postgres-password 574 | ingress: 575 | enabled: false 576 | # REQUIRED: Update to a hostname in a DNS domain you own 577 | # host: runtime-api.example.com 578 | className: traefik 579 | annotations: 580 | cert-manager.io/cluster-issuer: letsencrypt-production 581 | tls: true 582 | env: 583 | DB_HOST: oh-main-postgresql 584 | DB_USER: postgres 585 | DB_NAME: runtime_api_db 586 | K8S_NAMESPACE: "openhands" 587 | RUNTIME_CLASS: "" 588 | # REQUIRED: Update to match the host set in the ingress section above 589 | # RUNTIME_BASE_URL: "runtime.example.com" 590 | RUNTIME_DISABLE_SSL: "true" 591 | MEMORY_REQUEST: "3072Mi" 592 | MEMORY_LIMIT: "3072Mi" 593 | CPU_REQUEST: "500m" 594 | EPHEMERAL_STORAGE_SIZE: "10Gi" 595 | 596 | jira: 597 | enabled: false 598 | 599 | jiraDc: 600 | enabled: false 601 | 602 | linear: 603 | enabled: false 604 | 605 | global: 606 | security: 607 | # This allows using the bitnamilegacy image repo. 608 | # See: https://github.com/bitnami/containers/issues/83267 609 | allowInsecureImages: true 610 | -------------------------------------------------------------------------------- /charts/runtime-api/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: postgresql 3 | repository: https://charts.bitnami.com/bitnami 4 | version: 15.5.38 5 | - name: helm-release-pruner 6 | repository: https://charts.fairwinds.com/stable 7 | version: 3.3.0 8 | digest: sha256:9f911cd2f97b7cbaa03b13b538f1ee44e0fb589f40c797c71ccc032e3a669ba5 9 | generated: "2025-08-12T22:14:25.137674542-04:00" 10 | -------------------------------------------------------------------------------- /charts/runtime-api/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: runtime-api 3 | description: A Helm chart for the Flask application 4 | version: 0.1.16 # Change this to trigger a new helm chart version being published 5 | appVersion: "1.0.0" 6 | dependencies: 7 | - name: postgresql 8 | version: 15.x.x 9 | repository: https://charts.bitnami.com/bitnami 10 | condition: postgresql.enabled 11 | - name: helm-release-pruner 12 | version: 3.3.0 13 | repository: https://charts.fairwinds.com/stable 14 | condition: helm-release-pruner.enabled 15 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/_env.yaml: -------------------------------------------------------------------------------- 1 | {{- define "runtime-api.env" }} 2 | {{- range $key, $value := .Values.env }} 3 | - name: {{ $key }} 4 | value: {{ $value | quote }} 5 | {{- end }} 6 | - name: RUNTIME_DEAD_SECONDS 7 | value: {{ printf "%.0f" .Values.cleanup.dead_seconds | quote }} 8 | {{- if .Values.runtimeInSameCluster }} 9 | - name: RUNTIME_IN_SAME_CLUSTER 10 | value: "true" 11 | {{- end }} 12 | {{- if .Values.ingressBase.enabled }} 13 | - name: INGRESS_BASE 14 | value: "/ingress-base/base-ingress.yaml" 15 | {{- end }} 16 | {{- if .Values.certificate.enabled }} 17 | - name: RUNTIME_CERT_SECRET 18 | value: {{ .Values.certificate.secretName }} 19 | {{- end }} 20 | {{- if .Values.datadog.enabled }} 21 | # Datadog environment variables 22 | - name: DD_AGENT_HOST 23 | value: "datadog-agent.all-hands-system.svc.cluster.local" 24 | - name: DD_TRACE_AGENT_PORT 25 | value: "8126" 26 | - name: DD_DOGSTATSD_PORT 27 | value: "8125" 28 | - name: DD_SERVICE 29 | value: "runtime-api" 30 | - name: DD_ENV 31 | value: {{ .Values.datadog.env | default "dev" | quote }} 32 | - name: DD_LOGS_INJECTION 33 | value: "true" 34 | - name: DD_TRACE_ENABLED 35 | value: "true" 36 | - name: DD_TRACE_SAMPLING_RULES 37 | value: '[{"resource": "GET /health", "sample_rate": 0.0}, {"resource": "GET /internal/metrics", "sample_rate": 0.0}]' 38 | {{- end }} 39 | {{- if .Values.postgresql.enabled }} 40 | - name: DB_HOST 41 | value: "{{ .Release.Name }}-postgresql" 42 | - name: DB_USER 43 | value: "{{ .Values.postgresql.auth.username }}" 44 | - name: DB_NAME 45 | value: "{{ .Values.postgresql.auth.database }}" 46 | {{- else if .Values.database.create }} # should only be true for AWS deploys 47 | - name: DB_HOST 48 | value: "{{.Values.database.host}}" 49 | - name: DB_PORT 50 | value: "{{.Values.database.port}}" 51 | - name: DB_USER 52 | value: "{{.Values.database.new_user}}" 53 | - name: DB_NAME 54 | value: "{{.Values.database.name}}" 55 | {{- end }} 56 | - name: DB_PASS 57 | valueFrom: 58 | secretKeyRef: 59 | name: {{ .Values.postgresql.auth.existingSecret }} 60 | key: password 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "runtime-api.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "runtime-api.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "runtime-api.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "runtime-api.labels" -}} 37 | helm.sh/chart: {{ include "runtime-api.chart" . }} 38 | {{ include "runtime-api.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "runtime-api.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "runtime-api.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | PostgreSQL host 55 | */}} 56 | {{- define "runtime-api.postgresql.host" -}} 57 | {{- if .Values.postgresql.enabled }} 58 | {{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}} 59 | {{- else }} 60 | {{- .Values.database.host -}} 61 | {{- end }} 62 | {{- end -}} 63 | 64 | {{/* 65 | PostgreSQL username 66 | */}} 67 | {{- define "runtime-api.postgresql.username" -}} 68 | {{- if .Values.postgresql.enabled }} 69 | {{- .Values.postgresql.auth.username -}} 70 | {{- else }} 71 | {{- .Values.database.user -}} 72 | {{- end }} 73 | {{- end -}} 74 | 75 | {{/* 76 | PostgreSQL database 77 | */}} 78 | {{- define "runtime-api.postgresql.database" -}} 79 | {{- if .Values.postgresql.enabled }} 80 | {{- .Values.postgresql.auth.database -}} 81 | {{- else }} 82 | {{- .Values.database.name -}} 83 | {{- end }} 84 | {{- end -}} 85 | 86 | {{/* 87 | PostgreSQL secret name 88 | */}} 89 | {{- define "runtime-api.postgresql.secretName" -}} 90 | {{- if .Values.postgresql.enabled }} 91 | {{- printf "%s-%s" .Release.Name "postgresql" -}} 92 | {{- else }} 93 | {{- include "runtime-api.fullname" . -}} 94 | {{- end }} 95 | {{- end -}} 96 | 97 | {{/* 98 | PostgreSQL secret key 99 | */}} 100 | {{- define "runtime-api.postgresql.secretKey" -}} 101 | {{- if .Values.postgresql.enabled }} 102 | {{- printf "postgres-password" -}} 103 | {{- else }} 104 | {{- printf "db-password" -}} 105 | {{- end }} 106 | {{- end -}} 107 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/cleanup-cronjob.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if .Values.cleanup.enabled }} 3 | apiVersion: batch/v1 4 | kind: CronJob 5 | metadata: 6 | name: {{ include "runtime-api.fullname" . }}-cleanup 7 | spec: 8 | schedule: {{ .Values.cleanup.schedule | quote }} 9 | concurrencyPolicy: Forbid 10 | jobTemplate: 11 | spec: 12 | template: 13 | spec: 14 | {{- with .Values.securityContext }} 15 | securityContext: 16 | {{- toYaml . | nindent 12 }} 17 | {{- end }} 18 | serviceAccountName: {{ .Values.serviceAccount.name }} 19 | {{- with .Values.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 12 }} 22 | {{- end }} 23 | {{- if .Values.ingressBase.enabled }} 24 | volumes: 25 | - name: ingress-base-config 26 | configMap: 27 | name: {{ include "runtime-api.fullname" . }}-ingress-base 28 | {{- end }} 29 | containers: 30 | - name: cleanup 31 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 32 | command: ["python", "cleanup.py"] 33 | env: 34 | {{- include "runtime-api.env" . | nindent 14 }} 35 | - name: DB_POOL_SIZE 36 | value: {{ .Values.database.poolSize.cronjobs | default 2 | quote }} 37 | - name: DB_MAX_OVERFLOW 38 | value: {{ .Values.database.maxOverflow.cronjobs | default 5 | quote }} 39 | - name: RUNTIME_IDLE_SECONDS 40 | value: {{ .Values.cleanup.idle_seconds | default 1800 | quote }} 41 | {{- if .Values.ingressBase.enabled }} 42 | volumeMounts: 43 | - name: ingress-base-config 44 | mountPath: /ingress-base 45 | {{- end }} 46 | restartPolicy: OnFailure 47 | {{- end }} 48 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if and .Values.runtimeInSameCluster .Values.serviceAccount.createRBAC }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | name: {{ include "runtime-api.fullname" . }}-clusterrole 7 | labels: 8 | {{- include "runtime-api.labels" . | nindent 4 }} 9 | rules: 10 | - apiGroups: [""] 11 | resources: ["pods", "services", "namespaces", "serviceaccounts", "persistentvolumeclaims", "persistentvolumes"] 12 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 13 | - apiGroups: ["apps"] 14 | resources: ["deployments"] 15 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 16 | - apiGroups: [""] 17 | resources: ["pods/log"] 18 | verbs: ["get"] 19 | - apiGroups: ["networking.k8s.io"] 20 | resources: ["ingresses"] 21 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 22 | - apiGroups: ["policy"] 23 | resources: ["poddisruptionbudgets"] 24 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 25 | - apiGroups: ["traefik.io"] 26 | resources: ["middlewares"] 27 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 28 | - apiGroups: ["gateway.networking.k8s.io"] 29 | resources: ["httproutes", "gateways"] 30 | verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if and .Values.runtimeInSameCluster .Values.serviceAccount.createRBAC }} 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRoleBinding 5 | metadata: 6 | name: {{ include "runtime-api.fullname" . }}-clusterrolebinding 7 | labels: 8 | {{- include "runtime-api.labels" . | nindent 4 }} 9 | subjects: 10 | - kind: ServiceAccount 11 | name: {{ .Values.serviceAccount.name }} 12 | namespace: {{ .Release.Namespace }} 13 | roleRef: 14 | kind: ClusterRole 15 | name: {{ include "runtime-api.fullname" . }}-clusterrole 16 | apiGroup: rbac.authorization.k8s.io 17 | {{- end }} 18 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/create-db-user-job.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.database.create -}} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: create-postgres-user 6 | annotations: 7 | "helm.sh/hook": "pre-install,pre-upgrade" 8 | "helm.sh/hook-weight": "-10" 9 | spec: 10 | template: 11 | spec: 12 | {{- with .Values.securityContext }} 13 | securityContext: 14 | {{- toYaml . | nindent 8 }} 15 | {{- end }} 16 | containers: 17 | - name: create-user 18 | image: postgres:14 19 | env: 20 | - name: PGPASSWORD 21 | valueFrom: 22 | secretKeyRef: 23 | name: postgres-admin-secret 24 | key: postgres-admin-password 25 | - name: NEW_USER_PASSWORD 26 | valueFrom: 27 | secretKeyRef: 28 | name: postgres-password 29 | key: password 30 | command: 31 | - sh 32 | - -c 33 | - | 34 | # Create the database if it doesn't exist 35 | psql -h {{.Values.database.host}} -p {{.Values.database.port}} -U {{.Values.database.user}} -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='{{.Values.database.name}}'" | grep -q 1 || \ 36 | psql -h {{.Values.database.host}} -p {{.Values.database.port}} -U {{.Values.database.user}} -d postgres -c "CREATE DATABASE {{.Values.database.name}};" 37 | 38 | # Create the user if it doesn't exist 39 | psql -h {{.Values.database.host}} -p {{.Values.database.port}} -U {{.Values.database.user}} -d {{.Values.database.name}} -tc "SELECT 1 FROM pg_roles WHERE rolname='{{.Values.database.new_user}}'" | grep -q 1 || \ 40 | (psql -h {{.Values.database.host}} -p {{.Values.database.port}} -U {{.Values.database.user}} -d {{.Values.database.name}} -c "CREATE USER {{.Values.database.new_user}} WITH PASSWORD '$NEW_USER_PASSWORD'; GRANT ALL PRIVILEGES ON DATABASE {{.Values.database.name}} TO {{.Values.database.new_user}};") 41 | restartPolicy: Never 42 | backoffLimit: 3 43 | {{- end }} -------------------------------------------------------------------------------- /charts/runtime-api/templates/db-cleanup-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.dbCleanup.enabled (or .Values.postgresql.enabled .Values.database.secretName) }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-db-cleanup 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | spec: 9 | schedule: {{ .Values.dbCleanup.schedule | quote }} 10 | concurrencyPolicy: Forbid 11 | successfulJobsHistoryLimit: {{ .Values.dbCleanup.successfulJobsHistoryLimit }} 12 | failedJobsHistoryLimit: {{ .Values.dbCleanup.failedJobsHistoryLimit }} 13 | jobTemplate: 14 | spec: 15 | template: 16 | spec: 17 | {{- with .Values.securityContext }} 18 | securityContext: 19 | {{- toYaml . | nindent 12 }} 20 | {{- end }} 21 | serviceAccountName: {{ .Values.serviceAccount.name }} 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 12 }} 25 | {{- end }} 26 | {{- if .Values.ingressBase.enabled }} 27 | volumes: 28 | - name: ingress-base-config 29 | configMap: 30 | name: {{ include "runtime-api.fullname" . }}-ingress-base 31 | {{- end }} 32 | containers: 33 | - name: db-cleanup 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | command: ["python", "prune_db.py"] 37 | env: 38 | - name: RETENTION_DAYS 39 | value: {{ .Values.dbCleanup.retentionDays | quote }} 40 | {{- include "runtime-api.env" . | nindent 12 }} 41 | - name: DB_POOL_SIZE 42 | value: {{ .Values.database.poolSize.cronjobs | default 2 | quote }} 43 | - name: DB_MAX_OVERFLOW 44 | value: {{ .Values.database.maxOverflow.cronjobs | default 5 | quote }} 45 | {{- if .Values.ingressBase.enabled }} 46 | volumeMounts: 47 | - name: ingress-base-config 48 | mountPath: /ingress-base 49 | {{- end }} 50 | restartPolicy: OnFailure 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "runtime-api.fullname" . }} 5 | labels: 6 | {{- include "runtime-api.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "runtime-api.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- include "runtime-api.selectorLabels" . | nindent 8 }} 16 | spec: 17 | serviceAccountName: {{ .Values.serviceAccount.name }} 18 | {{- with .Values.securityContext }} 19 | securityContext: 20 | {{- toYaml . | nindent 8 }} 21 | {{- end }} 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | volumes: 27 | {{- if .Values.warmRuntimes.enabled }} 28 | - name: warm-runtimes-config 29 | configMap: 30 | name: {{ .Values.warmRuntimes.configMapName }} 31 | {{- end }} 32 | {{- if .Values.ingressBase.enabled }} 33 | - name: ingress-base-config 34 | configMap: 35 | name: {{ include "runtime-api.fullname" . }}-ingress-base 36 | {{- end }} 37 | containers: 38 | - name: {{ .Chart.Name }} 39 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 40 | imagePullPolicy: {{ .Values.image.pullPolicy }} 41 | {{- if .Values.datadog.enabled }} 42 | command: ["/bin/sh", "-c", "ddtrace-run python -m flask run --host=0.0.0.0"] 43 | {{- else }} 44 | command: ["/bin/sh", "-c", "python -m flask run --host=0.0.0.0"] 45 | {{- end }} 46 | ports: 47 | - name: http 48 | containerPort: 5000 49 | protocol: TCP 50 | env: 51 | {{- include "runtime-api.env" . | nindent 12 }} 52 | - name: DB_POOL_SIZE 53 | value: {{ .Values.database.poolSize.api | default 5 | quote }} 54 | - name: DB_MAX_OVERFLOW 55 | value: {{ .Values.database.maxOverflow.api | default 10 | quote }} 56 | {{- if .Values.defaultAPIKeySecretName }} 57 | - name: DEFAULT_API_KEY 58 | valueFrom: 59 | secretKeyRef: 60 | name: {{ .Values.defaultAPIKeySecretName }} 61 | key: default-api-key 62 | {{- end }} 63 | 64 | - name: ADMIN_PASSWORD 65 | valueFrom: 66 | secretKeyRef: 67 | name: admin-password 68 | key: admin-password 69 | 70 | 71 | resources: 72 | {{- toYaml .Values.resources | nindent 12 }} 73 | {{- if or .Values.warmRuntimes.enabled .Values.ingressBase.enabled }} 74 | volumeMounts: 75 | {{- if .Values.warmRuntimes.enabled }} 76 | - name: warm-runtimes-config 77 | mountPath: /config 78 | {{- end }} 79 | {{- if .Values.ingressBase.enabled }} 80 | - name: ingress-base-config 81 | mountPath: /ingress-base 82 | {{- end }} 83 | {{- end }} 84 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-hpa 6 | spec: 7 | scaleTargetRef: 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | name: {{ include "runtime-api.fullname" . }} 11 | minReplicas: {{ .Values.autoscaling.minReplicas }} 12 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/ingress-base-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingressBase.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-ingress-base 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | data: 9 | base-ingress.yaml: | 10 | {{- omit .Values.ingressBase "enabled" | toYaml | nindent 4 }} 11 | {{- end }} -------------------------------------------------------------------------------- /charts/runtime-api/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-ingress 6 | {{- with .Values.ingress.annotations }} 7 | annotations: 8 | {{- toYaml . | nindent 4 }} 9 | {{- end }} 10 | spec: 11 | {{- if .Values.ingress.className }} 12 | ingressClassName: {{ .Values.ingress.className }} 13 | {{- end }} 14 | {{- if .Values.ingress.tls }} 15 | tls: 16 | - hosts: 17 | {{- if .Values.ingress.prefixWithBranch }} 18 | - {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 19 | {{- else }} 20 | - {{ .Values.ingress.host }} 21 | {{- end }} 22 | secretName: {{ .Values.ingress.tlsSecretName | default (printf "%s-tls" (include "runtime-api.fullname" .)) }} 23 | {{- end }} 24 | rules: 25 | {{- if .Values.ingress.prefixWithBranch }} 26 | - host: {{ .Values.branchSanitized }}.{{ .Values.ingress.host }} 27 | {{- else }} 28 | - host: {{ .Values.ingress.host }} 29 | {{- end }} 30 | http: 31 | paths: 32 | - path: {{ .Values.ingress.path | default "/" }} 33 | pathType: {{ .Values.ingress.pathType | default "Prefix" }} 34 | backend: 35 | service: 36 | name: {{ include "runtime-api.fullname" . }} 37 | port: 38 | number: {{ .Values.service.port }} 39 | {{- end }} 40 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/migrate-db.job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "runtime-api.fullname" . }}-migrate-db 5 | labels: 6 | {{- include "runtime-api.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": "post-install,post-upgrade" 9 | "helm.sh/hook-weight": "-5" 10 | {{- if .Values.postgresql.isFeatureDeploy }} 11 | "helm.sh/hook-delete-policy": "hook-succeeded" 12 | {{- else }} 13 | "helm.sh/hook-delete-policy": "before-hook-creation" 14 | {{- end }} 15 | spec: 16 | template: 17 | metadata: 18 | name: {{ include "runtime-api.fullname" . }}-migrate-db 19 | labels: 20 | {{- include "runtime-api.selectorLabels" . | nindent 8 }} 21 | spec: 22 | serviceAccountName: {{ .Values.serviceAccount.name }} 23 | restartPolicy: OnFailure 24 | {{- with .Values.securityContext }} 25 | securityContext: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | {{- with .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml . | nindent 8 }} 31 | {{- end }} 32 | {{- if .Values.ingressBase.enabled }} 33 | volumes: 34 | - name: ingress-base-config 35 | configMap: 36 | name: {{ include "runtime-api.fullname" . }}-ingress-base 37 | {{- end }} 38 | {{- if .Values.externalDatabase.enabled }} 39 | initContainers: 40 | - name: check-db-exists 41 | image: "bitnamilegacy/postgresql:latest" 42 | command: ['sh', '-c'] 43 | args: 44 | - | 45 | echo "Checking if the DB \"$DB_NAME\" and user \"$DB_USER\" exist..." 46 | export PGPASSWORD=$DB_ADMIN_PASSWORD 47 | # Create the database if it doesn't exist 48 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || \ 49 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d postgres -c "CREATE DATABASE $DB_NAME;" 50 | 51 | # Create the user if it doesn't exist 52 | psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $DB_NAME -tc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || \ 53 | (psql -h $DB_HOST -p 5432 -U $DB_ADMIN_USER -d $DB_NAME -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS'; GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;") 54 | env: 55 | - name: DB_ADMIN_PASSWORD 56 | valueFrom: 57 | secretKeyRef: 58 | name: {{ .Values.externalDatabase.existingSecret }} 59 | key: admin_password 60 | {{- include "runtime-api.env" . | nindent 12 }} 61 | {{- end }} 62 | containers: 63 | - name: migrate-db 64 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 65 | command: ["alembic", "upgrade", "head"] 66 | env: 67 | {{- include "runtime-api.env" . | nindent 12 }} 68 | {{- if .Values.ingressBase.enabled }} 69 | volumeMounts: 70 | - name: ingress-base-config 71 | mountPath: /ingress-base 72 | {{- end }} 73 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/monitoring.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.monitoring.enabled }} 2 | apiVersion: monitoring.googleapis.com/v1 3 | kind: PodMonitoring 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-monitoring 6 | spec: 7 | selector: 8 | matchLabels: 9 | {{- include "runtime-api.selectorLabels" . | nindent 6 }} 10 | endpoints: 11 | - port: 5000 12 | interval: 30s 13 | path: /metrics 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/pod-status-logger-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podStatusLogger.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . | trunc 33 | trimSuffix "-" }}-pod-status-logger 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | spec: 9 | schedule: {{ .Values.podStatusLogger.schedule | quote }} 10 | concurrencyPolicy: Forbid 11 | jobTemplate: 12 | spec: 13 | template: 14 | spec: 15 | {{- with .Values.securityContext }} 16 | securityContext: 17 | {{- toYaml . | nindent 12 }} 18 | {{- end }} 19 | serviceAccountName: {{ .Values.serviceAccount.name }} 20 | {{- with .Values.imagePullSecrets }} 21 | imagePullSecrets: 22 | {{- toYaml . | nindent 12 }} 23 | {{- end }} 24 | {{- if .Values.ingressBase.enabled }} 25 | volumes: 26 | - name: ingress-base-config 27 | configMap: 28 | name: {{ include "runtime-api.fullname" . }}-ingress-base 29 | {{- end }} 30 | containers: 31 | - name: pod-status-logger 32 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 33 | imagePullPolicy: {{ .Values.image.pullPolicy }} 34 | command: ["python", "pod_status_logger.py"] 35 | env: 36 | {{- include "runtime-api.env" . | nindent 12 }} 37 | {{- if .Values.ingressBase.enabled }} 38 | volumeMounts: 39 | - name: ingress-base-config 40 | mountPath: /ingress-base 41 | {{- end }} 42 | restartPolicy: OnFailure 43 | {{- end }} 44 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "runtime-api.fullname" . }} 5 | labels: 6 | {{- include "runtime-api.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "runtime-api.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ .Values.serviceAccount.name }} 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | {{- if .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- range $key, $value := .Values.serviceAccount.annotations }} 11 | "{{ $key }}": "{{ $value }}" 12 | {{- end }} 13 | {{- end }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/warm-runtimes-configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.warmRuntimes.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ .Values.warmRuntimes.configMapName }} 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | data: 9 | warm-runtimes.json: | 10 | {{- dict "count" .Values.warmRuntimes.count "configs" .Values.warmRuntimes.configs | toJson | nindent 4 }} 11 | {{- end }} 12 | -------------------------------------------------------------------------------- /charts/runtime-api/templates/warm-runtimes-cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.warmRuntimes.enabled }} 2 | apiVersion: batch/v1 3 | kind: CronJob 4 | metadata: 5 | name: {{ include "runtime-api.fullname" . }}-warm-runtimes 6 | labels: 7 | {{- include "runtime-api.labels" . | nindent 4 }} 8 | spec: 9 | schedule: "* * * * *" # Run every minute 10 | concurrencyPolicy: Forbid 11 | jobTemplate: 12 | spec: 13 | template: 14 | metadata: 15 | labels: 16 | {{- include "runtime-api.selectorLabels" . | nindent 12 }} 17 | spec: 18 | {{- with .Values.securityContext }} 19 | securityContext: 20 | {{- toYaml . | nindent 12 }} 21 | {{- end }} 22 | serviceAccountName: {{ .Values.serviceAccount.name }} 23 | restartPolicy: OnFailure 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 12}} 27 | {{- end }} 28 | containers: 29 | - name: warm-runtimes 30 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 31 | imagePullPolicy: {{ .Values.image.pullPolicy }} 32 | command: ["python", "/app/warm_runtimes.py"] 33 | env: 34 | {{- include "runtime-api.env" . | nindent 16 }} 35 | volumeMounts: 36 | - name: warm-runtimes-config 37 | mountPath: /config 38 | {{- if .Values.ingressBase.enabled }} 39 | - name: ingress-base-config 40 | mountPath: /ingress-base 41 | {{- end }} 42 | volumes: 43 | - name: warm-runtimes-config 44 | configMap: 45 | name: {{ .Values.warmRuntimes.configMapName }} 46 | {{- if .Values.ingressBase.enabled }} 47 | - name: ingress-base-config 48 | configMap: 49 | name: {{ include "runtime-api.fullname" . }}-ingress-base 50 | {{- end }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /charts/runtime-api/values.yaml: -------------------------------------------------------------------------------- 1 | nameOverride: "" 2 | fullnameOverride: "" 3 | 4 | replicaCount: 1 5 | 6 | image: 7 | repository: ghcr.io/all-hands-ai/runtime-api 8 | tag: sha-07c8732 9 | pullPolicy: Always 10 | 11 | imagePullSecrets: 12 | - name: ghcr-login-secret 13 | 14 | serviceAccount: 15 | create: true 16 | name: runtime-api-sa 17 | annotations: {} 18 | createRBAC: true 19 | 20 | runtimeInSameCluster: false 21 | defaultAPIKeySecretName: default-api-key 22 | 23 | service: 24 | type: ClusterIP 25 | port: 5000 26 | 27 | env: 28 | PYTHONDONTWRITEBYTECODE: "1" 29 | PYTHONUNBUFFERED: "1" 30 | DEV_MODE: "false" 31 | ARTIFACT_REGISTRY_REPOSITORY: "runtime-api-docker-repo" 32 | ARTIFACT_REGISTRY_LOCATION: "us-central1" 33 | GCP_REGION: "us-central1" 34 | STORAGE_CLASS: "standard-rwo" 35 | INCLUDE_START_PROBE: "true" 36 | INCLUDE_LIVENESS_PROBE: "true" 37 | RUNTIME_CLASS: "sysbox-runc" 38 | 39 | resources: 40 | limits: 41 | cpu: 1 42 | memory: 4Gi 43 | requests: 44 | cpu: 1 45 | memory: 4Gi 46 | 47 | # Security context settings for all pods 48 | securityContext: 49 | runAsUser: 1000 50 | runAsNonRoot: true 51 | 52 | nodeSelector: {} 53 | 54 | tolerations: [] 55 | 56 | affinity: {} 57 | 58 | postgresql: 59 | enabled: false 60 | isFeatureDeploy: false 61 | primary: 62 | persistence: 63 | enabled: false 64 | auth: 65 | username: postgres 66 | password: "" # This should be set externally for security reasons 67 | existingSecret: postgres-password 68 | database: runtime_api_db 69 | service: 70 | ports: 71 | postgresql: 5432 72 | image: 73 | repository: bitnamilegacy/postgresql 74 | 75 | externalDatabase: 76 | enabled: false 77 | existingSecret: postgres-password 78 | 79 | # External database configuration (used when postgresql.enabled=false) 80 | database: 81 | create: false # Will only be true for AWS as it can't create the DB in terraform 82 | secretName: runtime-api-db-credentials # Secret containing external database credentials 83 | poolSize: 84 | api: 5 # Pool size for API deployment (default) 85 | cronjobs: 2 # Pool size for cronjobs (default) 86 | maxOverflow: 87 | api: 10 # Max overflow for API deployment (default) 88 | cronjobs: 5 # Max overflow for cronjobs (default) 89 | 90 | helm-release-pruner: 91 | enabled: false 92 | job: 93 | schedule: "0 */4 * * *" 94 | dryRun: False 95 | pruneProfiles: 96 | - olderThan: "1 days ago" 97 | helmReleaseFilter: "^runtime-api-.*$" 98 | namespaceFilter: "^runtime-api-.*$" 99 | maxReleasesToKeep: 10 100 | 101 | certificate: 102 | enabled: false 103 | 104 | ingress: 105 | enabled: false 106 | host: chart-example.local 107 | className: traefik 108 | annotations: 109 | cert-manager.io/cluster-issuer: letsencrypt 110 | path: / 111 | pathType: Prefix 112 | tls: true 113 | 114 | # Base ingress configuration for runtime ingress inheritance 115 | # Only metadata (labels and annotations) will be inherited, not the spec 116 | ingressBase: 117 | enabled: false 118 | apiVersion: networking.k8s.io/v1 119 | kind: Ingress 120 | metadata: 121 | name: base-ingress 122 | labels: 123 | app: runtime-api 124 | environment: production 125 | team: platform 126 | annotations: 127 | kubernetes.io/ingress.class: nginx 128 | nginx.ingress.kubernetes.io/ssl-redirect: "true" 129 | cert-manager.io/cluster-issuer: letsencrypt-prod 130 | 131 | autoscaling: 132 | enabled: true 133 | minReplicas: 3 134 | maxReplicas: 6 135 | targetCPUUtilizationPercentage: 75 136 | 137 | cleanup: 138 | enabled: true 139 | schedule: "*/5 * * * *" 140 | idle_seconds: 1800 # Default to 30 minutes 141 | dead_seconds: 86400 # Default to 1 day 142 | 143 | warmRuntimes: 144 | enabled: false 145 | configMapName: warm-runtimes-config 146 | count: 0 147 | configs: 148 | - name: default 149 | image: "ghcr.io/all-hands-ai/runtime:3c39b93f7e151f88e7b4274b80aafa604fcae092-nikolaik" 150 | working_dir: "/openhands/code/" 151 | environment: {} 152 | command: 153 | - /openhands/micromamba/bin/micromamba 154 | - run 155 | - -n 156 | - openhands 157 | - poetry 158 | - run 159 | - python 160 | - -u 161 | - -m 162 | - openhands.runtime.action_execution_server 163 | - "60000" 164 | - --working-dir 165 | - /workspace 166 | - --plugins 167 | - agent_skills 168 | - jupyter 169 | - vscode 170 | - --username 171 | - root 172 | - --user-id 173 | - "0" 174 | 175 | # Database cleanup CronJob configuration 176 | dbCleanup: 177 | enabled: true 178 | schedule: "0 0 * * *" # Run daily at midnight 179 | retentionDays: 90 # Number of days to retain runtime objects 180 | successfulJobsHistoryLimit: 3 181 | failedJobsHistoryLimit: 1 182 | 183 | # Pod Status Logger configuration 184 | podStatusLogger: 185 | enabled: true 186 | schedule: "*/5 * * * *" # Run every 5 minutes 187 | 188 | monitoring: 189 | enabled: true 190 | 191 | datadog: 192 | enabled: false 193 | env: "dev" 194 | 195 | global: 196 | security: 197 | # This allows using the bitnamilegacy image repo. 198 | # See: https://github.com/bitnami/containers/issues/83267 199 | allowInsecureImages: true 200 | -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | OpenHands Cloud is deployed to a Kubernetes cluster using Helm charts, with minimal dependence on other infrastructure. 4 | 5 | ![Figure 1](./assets/fig1.svg) 6 | 7 | ## OpenHands components 8 | 9 | **OpenHands** has the main entrypoints for using OpenHands agents. This includes the user interface as well as the conversations API and webhooks such as the GitHub / GitLab resolvers. 10 | 11 | **Runtime API** manages the Runtime pods. It maintains a pool of waiting runtime pods (called "warm runtimes") and assigns them to conversations on request. 12 | 13 | **Runtimes** are the isolated execution environments that run OpenHands agents. 14 | 15 | **Image Loader** is a daemon that ensures the Runtime image is cached on every node in the Runtime cluster 16 | 17 | ## Third-party components 18 | 19 | * [Kubernetes](https://kubernetes.io) (compute for OpenHands UI, agent and runtimes) 20 | * [PostgreSQL](https://www.postgresql.org) (databases for several of the components) 21 | * [Redis](https://redis.io) (caching) 22 | * [Keycloak](https://www.keycloak.org) (authentication) 23 | * [LiteLLM Proxy](https://docs.litellm.ai/docs/simple_proxy) (handles connections to AI Language models) 24 | * [Langfuse](https://langfuse.com) (LLM monitoring) 25 | * [Clickhouse](https://clickhouse.com) (event-store used by LangFuses) 26 | * AI Language Models 27 | 28 | ## Additional Infrastructure 29 | 30 | These will be implemented differently depending on which cloud provider you use. 31 | 32 | * Load balancer 33 | * Conversation bucket (usually a blob-store like S3) 34 | -------------------------------------------------------------------------------- /docs/assets/fig1.d2: -------------------------------------------------------------------------------- 1 | # Use https://play.d2lang.com to edit with preview (layout engine ELK) 2 | # To update the SVG, run: (cd docs && ./build-diagrams.sh) 3 | 4 | direction: right 5 | 6 | classes: { 7 | all-hands: { 8 | style: { 9 | double-border: true 10 | } 11 | } 12 | } 13 | 14 | app-cluster: Kubernetes Cluster { 15 | openhands: OpenHands { 16 | class: all-hands 17 | } 18 | runtime-api: Runtime API { 19 | class: all-hands 20 | } 21 | 22 | keycloak: Keycloak 23 | postgres: PostgreSQL { 24 | shape: cylinder 25 | } 26 | langfuse: LangFuse 27 | litellm: LiteLLM Proxy 28 | redis: Redis { 29 | shape: cylinder 30 | } 31 | clickhouse: Clickhouse { 32 | shape: cylinder 33 | } 34 | 35 | runtimes: Runtimes { 36 | class: all-hands 37 | } 38 | image-loader: Image Loader { 39 | class: all-hands 40 | } 41 | } 42 | convo-bucket: Conversation\n Bucket { 43 | shape: cylinder 44 | } 45 | llms: AI Language\n Models { 46 | shape: cloud 47 | } 48 | lb: Load\nBalancer { 49 | shape: cloud 50 | } 51 | 52 | lb -> app-cluster.runtimes 53 | app-cluster.litellm -> llms 54 | 55 | lb -> app-cluster.openhands 56 | lb -> app-cluster.keycloak 57 | app-cluster.openhands -> app-cluster.runtime-api 58 | app-cluster.openhands -> app-cluster.keycloak 59 | app-cluster.openhands -> app-cluster.redis 60 | app-cluster.openhands -> convo-bucket 61 | app-cluster.runtime-api -> app-cluster.runtimes 62 | app-cluster.litellm -> app-cluster.langfuse 63 | app-cluster.langfuse -> app-cluster.clickhouse 64 | app-cluster.openhands -> app-cluster.litellm 65 | 66 | app-cluster.openhands -> app-cluster.postgres 67 | app-cluster.litellm -> app-cluster.postgres 68 | app-cluster.keycloak -> app-cluster.postgres 69 | app-cluster.runtime-api -> app-cluster.postgres 70 | 71 | app-cluster.runtimes -> app-cluster.openhands 72 | 73 | app-cluster.runtimes -- app-cluster.image-loader { 74 | style: { 75 | stroke-dash: 3 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/assets/fig1.svg: -------------------------------------------------------------------------------- 1 | Kubernetes ClusterConversation BucketAI Language ModelsLoadBalancerOpenHandsRuntime APIKeycloakPostgreSQLLangFuseLiteLLM ProxyRedisClickhouseRuntimesImage Loader 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /docs/build-diagrams.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # Go into docs folder if we're not already there. 6 | cd docs || true 7 | 8 | which d2 || (echo "d2 command not found, see: https://github.com/terrastruct/d2" ; exit 1) 9 | export D2_LAYOUT=elk 10 | 11 | d2 assets/fig1.d2 assets/fig1.svg 12 | --------------------------------------------------------------------------------