From 0bcd525a2c541b7390cb6048dfcdc8372559ad07 Mon Sep 17 00:00:00 2001 From: Richard Boisvert Date: Sun, 22 Feb 2026 12:46:58 -0500 Subject: [PATCH] [DEVOPS-4291] feat: add initial helm chart for deploying DVLS --- .github/workflows/release.yml | 89 ++++++ .gitignore | 8 + CLAUDE.md | 67 +++++ LICENSE | 201 +++++++++++++ README.md | 46 ++- chart/.helmignore | 3 + chart/Chart.yaml | 15 + chart/README.md | 315 ++++++++++++++++++++ chart/templates/NOTES.txt | 20 ++ chart/templates/_helpers.tpl | 136 +++++++++ chart/templates/backendtlspolicy.yaml | 21 ++ chart/templates/certificate.yaml | 17 ++ chart/templates/deployment.yaml | 179 +++++++++++ chart/templates/destinationrule.yaml | 15 + chart/templates/hooks/db-migration-job.yaml | 134 +++++++++ chart/templates/hooks/scale-down-job.yaml | 78 +++++ chart/templates/hooks/scale-down-rbac.yaml | 53 ++++ chart/templates/httproute.yaml | 26 ++ chart/templates/service.yaml | 16 + chart/values.yaml | 203 +++++++++++++ 20 files changed, 1641 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 LICENSE create mode 100644 chart/.helmignore create mode 100644 chart/Chart.yaml create mode 100644 chart/README.md create mode 100644 chart/templates/NOTES.txt create mode 100644 chart/templates/_helpers.tpl create mode 100644 chart/templates/backendtlspolicy.yaml create mode 100644 chart/templates/certificate.yaml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/destinationrule.yaml create mode 100644 chart/templates/hooks/db-migration-job.yaml create mode 100644 chart/templates/hooks/scale-down-job.yaml create mode 100644 chart/templates/hooks/scale-down-rbac.yaml create mode 100644 chart/templates/httproute.yaml create mode 100644 chart/templates/service.yaml create mode 100644 chart/values.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f081213 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: release + +on: + workflow_dispatch: + push: + branches: [master] + paths: ['chart/Chart.yaml'] + +jobs: + create-release: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.get-version.outputs.version }} + + steps: + - name: Check out ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Check out Devolutions/actions + uses: actions/checkout@v4 + with: + repository: Devolutions/actions + ref: v1 + token: ${{ secrets.DEVOLUTIONSBOT_TOKEN }} + path: ./.github/workflows + + - name: Set version + id: get-version + run: echo "version=$(grep '^version:' chart/Chart.yaml | awk '{print $2}')" >> "$GITHUB_OUTPUT" + + - name: Check tag does not already exist + run: | + if gh release view "v${{ steps.get-version.outputs.version }}" &>/dev/null; then + echo "::error::Release v${{ steps.get-version.outputs.version }} already exists. Bump the version in chart/Chart.yaml first." + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create release + uses: ./.github/workflows/create-release + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + tag: v${{ steps.get-version.outputs.version }} + + publish-helm-chart: + runs-on: ubuntu-latest + needs: create-release + environment: helm-publish + + steps: + - name: Check out ${{ github.repository }} + uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Package chart + run: helm package chart/ + + - name: Check out helm-charts repository + uses: actions/checkout@v4 + with: + repository: Devolutions/helm-charts + token: ${{ secrets.DEVOLUTIONSBOT_WRITE_TOKEN }} + path: helm-charts + + - name: Copy chart package + run: | + mkdir -p helm-charts/devolutions-server + cp devolutions-server-*.tgz helm-charts/devolutions-server/ + + - name: Update index + working-directory: helm-charts + run: | + if [ -f index.yaml ]; then + helm repo index . --url https://devolutions.github.io/helm-charts --merge index.yaml + else + helm repo index . --url https://devolutions.github.io/helm-charts + fi + + - name: Commit and push + working-directory: helm-charts + run: | + git config user.name "devolutionsbot" + git config user.email "bot@devolutions.net" + git add devolutions-server/devolutions-server-*.tgz index.yaml + git commit -m "feat(devolutions-server): add version ${{ needs.create-release.outputs.version }}" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff4dafd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# OS generated files +.DS_Store + +# Helm +*.tgz + +# Environment-specific values (contain infrastructure details) +chart/environments/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2c16a0e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Helm chart for deploying [Devolutions Server (DVLS)](https://devolutions.net/server/) on Kubernetes. The chart lives under `chart/` and produces Kubernetes Deployment, Service, Certificate, and optional networking resources (HTTPRoute, BackendTLSPolicy, DestinationRule). + +## Common Commands + +```bash +# Lint the chart +helm lint chart/ + +# Render templates locally (all four required values must be set) +helm template test chart/ \ + --set dvls.hostname=test.example.com \ + --set database.host=db.example.com \ + --set database.name=testdb \ + --set certificate.issuerName=letsencrypt +``` + +There are no unit tests or automated linting CI yet — validation is done manually with `helm lint` and `helm template`. + +## Architecture + +### Chart Structure + +- `chart/Chart.yaml` — chart metadata; `version` is the chart version, `appVersion` is the DVLS image version +- `chart/values.yaml` — all configurable values with defaults +- `chart/templates/_helpers.tpl` — shared template helpers (naming, labels, image tags, required-value validation) +- `chart/templates/` — core Kubernetes manifests (deployment, service, certificate) +- `chart/templates/hooks/` — pre-upgrade Helm hooks for database migrations +- `chart/environments/` — per-environment value overrides (gitignored) + +### Required Values + +Four values have no defaults and are enforced with `required` in helpers: +- `dvls.hostname` — external FQDN for the DVLS instance +- `database.host` — SQL Server hostname +- `database.name` — database name +- `certificate.issuerName` — cert-manager ClusterIssuer name (when `certificate.enabled: true`) + +### Database Migration Hooks + +Pre-upgrade hooks run in weight order to handle migrations safely: +1. **weight -15** `scale-down-rbac.yaml` — creates ServiceAccount/Role/RoleBinding +2. **weight -10** `scale-down-job.yaml` — scales deployment to 0 and waits for rollout +3. **weight -5** `db-migration-job.yaml` — runs DVLS with `DVLS_UPDATE_MODE=true` + +Migrations are enabled by default (`migration.enabled: true`) and can be skipped with `--set migration.enabled=false`. + +### Database Environment Variable Prefix + +The chart supports two env-var prefixes controlled by `database.envPrefix`: `DATABASE` (default) or `AZURE_SQL`. This determines whether database credentials are exposed as `DATABASE_USERNAME`/`DATABASE_PASSWORD` or `AZURE_SQL_USERNAME`/`AZURE_SQL_PASSWORD`. + +### Selector Label Override + +`selectorLabels` in values.yaml overrides the default selector labels on Deployment, Service, and hooks. This is used for migrating from an existing release that used different labels (e.g., `app: dvls` instead of `app: devolutions-server`). + +### TLS Configuration + +When `certificate.enabled: true`, the chart creates a cert-manager Certificate resource and mounts the resulting TLS secret into the pod at `/etc/certs/`. When disabled, an existing secret can be referenced via `certificate.secretName`. + +### Release Workflow + +`.github/workflows/release.yml` is triggered manually (`workflow_dispatch`). It creates a GitHub release tagged from `chart/Chart.yaml` version, packages the chart, and publishes it to the [Devolutions Helm repository](https://devolutions.github.io/helm-charts). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 5de9069..b9f922e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ # devolutions-server-helm -Devolutions Server Helm Chart + +Helm chart for deploying [Devolutions Server](https://devolutions.net/server/) on Kubernetes. + +## Chart Documentation + +See [chart/README.md](chart/README.md) for full installation instructions, values reference, and upgrade procedures. + +## Development + +### Prerequisites + +- [Helm](https://helm.sh/docs/intro/install/) 3.x + +### Lint + +```bash +helm lint chart/ +``` + +### Template rendering + +```bash +helm template test chart/ \ + --set dvls.hostname=test.example.com \ + --set database.host=db.example.com \ + --set database.name=testdb \ + --set certificate.issuerName=letsencrypt +``` + +## Releasing + +Releases are automated via GitHub Actions. Trigger the `release` workflow to: + +1. Create a GitHub release tagged with the chart version from `chart/Chart.yaml` +2. Package and publish the chart to the [Devolutions Helm repository](https://devolutions.github.io/helm-charts) + +## License + +Copyright 2026 Devolutions Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 0000000..50eb02e --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,3 @@ +.git +.gitignore +CHANGELOG.md diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..be3f0c6 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: devolutions-server +version: 2025.3.15 +description: Devolutions Server (DVLS) Helm chart +type: application +keywords: + - devolutions + - dvls + - password-manager +home: https://devolutions.net/server/ +maintainers: + - name: DevOps + url: https://forum.devolutions.net/forums/186/devolutions-server--linux +icon: https://cdnweb.devolutions.net/cdn-cgi/image/f=auto,w=200,onerror=redirect/web/common/images/icons/sys-secure-access.png +appVersion: '2025.3.15.0' diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 0000000..7e37e84 --- /dev/null +++ b/chart/README.md @@ -0,0 +1,315 @@ +# Devolutions Server Helm Chart + +A Helm chart for deploying [Devolutions Server](https://devolutions.net/server/) (DVLS) on Kubernetes. + +Devolutions Server is a self-hosted privileged access management (PAM) solution for managing passwords, credentials, and privileged accounts. + +## Features + +- **Automatic Database Migrations**: Pre-upgrade hooks scale down the deployment, run migrations, then deploy the new version +- **Environment-specific Values**: Use separate value files per environment for clean configuration management +- **TLS Support**: Integration with cert-manager for automatic certificate management +- **Gateway API / Istio Integration**: Optional HTTPRoute and DestinationRule for service mesh setups + +## Prerequisites + +- Kubernetes 1.32+ (tested with 1.34) +- Helm 3.17+ or v4 +- A SQL Server database (Azure SQL or self-hosted) +- A TLS certificate — DVLS serves HTTPS only. Use [cert-manager](https://cert-manager.io/) for automatic management, or provide a pre-existing TLS secret via `certificate.secretName` +- [Gateway API](https://gateway-api.sigs.k8s.io/) controller (optional, for HTTPRoute ingress) +- [Istio](https://istio.io/) (optional, for DestinationRule TLS origination) + +## Installation + +### Add the Helm repository + +```bash +helm repo add devolutions https://devolutions.github.io/helm-charts +helm repo update +``` + +### Create the required secrets + +The chart expects two Kubernetes secrets to exist before installation. Create them using your preferred method (Terraform, Vault, sealed-secrets, etc.). The `kubectl` examples below are for illustration only. + +**Docker Hub registry credentials** (referenced by `imagePullSecrets`): + +```bash +kubectl create secret docker-registry docker-hub \ + --docker-server=https://index.docker.io/v1/ \ + --docker-username='' \ + --docker-password='' +``` + +**DVLS credentials** (referenced by `existingSecret`): + +```bash +kubectl create secret generic devolutions-server \ + --from-literal=dvls-admin-password='' \ + --from-literal=dvls-encryption-config='' \ + --from-literal=db-username='' \ + --from-literal=db-password='' +``` + +The secret must contain the following keys: + +| Key | Description | +|-----|-------------| +| `dvls-admin-password` | Admin account password | +| `dvls-encryption-config` | Base64-encoded encryption configuration | +| `db-username` | Database username | +| `db-password` | Database password | + +> **Note:** The database key names `db-username` and `db-password` are defaults. You can use any key names by setting `database.usernameSecretKey` and `database.passwordSecretKey`. The DVLS container supports both `DATABASE_*` (default) and `AZURE_SQL_*` environment variable prefixes — set `database.envPrefix` to switch. + +To obtain the encryption configuration, follow the [Devolutions Server first-time setup](https://docs.devolutions.net/server/kb/how-to-articles/devolutions-server-docker-deployment/#devolutions-server-first-time-setup) guide. You can select your OS at the top of the documentation page. + +### Create an environment values file + +Create a values file for your environment (e.g. `values-production.yaml`): + +```yaml +replicaCount: 1 + +# Overrides the image tag whose default is the chart appVersion +image: + tag: '2025.3.15.0' + +imagePullSecrets: + - name: docker-hub + +dvls: + hostname: dvls.example.com + admin: + email: admin@example.com + +database: + host: sqlserver.example.com + name: dvls-db + +aspnetcore: + environment: Production + +certificate: + issuerName: letsencrypt + secretName: cert-dvls-example-com + +# Optional: Gateway API HTTPRoute +httproute: + enabled: true + gateway: + name: my-gateway + namespace: istio-system + sectionName: https-dvls + +# Optional: Istio DestinationRule for TLS origination +destinationRule: + enabled: true + +nodeSelector: + workload: apps + +existingSecret: devolutions-server +``` + +### Install the chart + +```bash +helm upgrade --install dvls devolutions/devolutions-server \ + -f values-production.yaml \ + -n devolutions-server --create-namespace \ + --wait --timeout 15m +``` + +## Previewing Changes + +Before applying changes, you can preview what will be modified using the [helm-diff plugin](https://github.com/databus23/helm-diff) (requires Helm 3.17+ or 4.x): + +```bash +helm diff upgrade dvls devolutions/devolutions-server \ + -f values-production.yaml \ + -n devolutions-server +``` + +## Upgrading + +The chart includes pre-upgrade migration hooks. When `migration.enabled=true` (the default), the upgrade process: + +1. **RBAC setup** (hook weight -15): Creates ServiceAccount, Role, and RoleBinding for migration jobs +2. **Scale down** (hook weight -10): Scales deployment to 0 replicas and waits for pods to terminate +3. **Migration** (hook weight -5): Runs database migration with `DVLS_UPDATE_MODE=true` +4. **Deploy**: Updates deployment with new image version + +To upgrade, update the `image.tag` in your values file and run: + +```bash +helm upgrade dvls devolutions/devolutions-server \ + -f values-production.yaml \ + -n devolutions-server \ + --wait --timeout 15m +``` + +Use `--wait` so Helm only returns when the Deployment is ready (based on the readiness probe). + +### Skipping Migrations + +For hotfixes that don't require database changes: + +```bash +helm upgrade dvls devolutions/devolutions-server \ + -f values-production.yaml \ + --set migration.enabled=false \ + -n devolutions-server \ + --wait --timeout 15m +``` + +### Rollback + +```bash +helm rollback dvls -n devolutions-server +``` + +> **Note:** If the upgrade included a database migration, you must restore the database from a snapshot **before** rolling back the image. See [Migration job failing](#migration-job-failing) for details. + +## Values Reference + +### Image + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.repository` | Container image repository | `devolutions/devolutions-server` | +| `image.tag` | Image tag (defaults to `appVersion`) | `""` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `imagePullSecrets` | Image pull secrets | `[]` | + +### General + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `nameOverride` | Override chart name | `""` | +| `fullnameOverride` | Override release fullname | `""` | +| `selectorLabels` | Override selector labels (for migration from existing releases) | `{}` | + +### DVLS Configuration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `dvls.hostname` | External hostname for DVLS (**required**) | `""` | +| `dvls.admin.username` | Admin username | `dvls-admin` | +| `dvls.admin.email` | Admin email address | `""` | +| `dvls.admin.passwordSecretKey` | Key in `existingSecret` for admin password | `dvls-admin-password` | +| `dvls.path` | DVLS path inside container | `/opt/devolutions/dvls` | +| `dvls.telemetry` | Enable telemetry | `false` | +| `dvls.encryptionConfigSecretKey` | Key in `existingSecret` for encryption config | `dvls-encryption-config` | + +### Database + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `database.host` | SQL Server hostname (**required**) | `""` | +| `database.name` | Database name (**required**) | `""` | +| `database.port` | Database port | `1433` | +| `database.envPrefix` | Environment variable prefix (`DATABASE` or `AZURE_SQL`) | `DATABASE` | +| `database.usernameSecretKey` | Key in `existingSecret` for DB username | `db-username` | +| `database.passwordSecretKey` | Key in `existingSecret` for DB password | `db-password` | + +### TLS Certificate + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `certificate.enabled` | Create a cert-manager Certificate resource | `true` | +| `certificate.name` | Certificate resource name | `-tls` | +| `certificate.secretName` | TLS secret name | `-tls` | +| `certificate.issuerName` | cert-manager ClusterIssuer name (**required** when certificate enabled) | `""` | +| `certificate.issuerKind` | Issuer kind | `ClusterIssuer` | +| `certificate.privateKeyRotationPolicy` | Private key rotation policy | `Always` | + +### Networking + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `service.type` | Service type | `ClusterIP` | +| `service.port` | Service port | `5000` | +| `service.targetPort` | Container target port | `5000` | +| `httproute.enabled` | Create a Gateway API HTTPRoute | `false` | +| `httproute.gateway.name` | Gateway name | `""` | +| `httproute.gateway.namespace` | Gateway namespace | `""` | +| `httproute.gateway.sectionName` | Gateway section name | `""` | +| `backendTLSPolicy.enabled` | Create a Gateway API BackendTLSPolicy | `false` | +| `backendTLSPolicy.wellKnownCACertificates` | Use well-known CAs (e.g., `System`) | `""` | +| `backendTLSPolicy.caCertificateRefs` | CA certificate refs for backend TLS validation | `[]` | +| `destinationRule.enabled` | Create an Istio DestinationRule | `false` | + +### Resources and Scheduling + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `replicaCount` | Number of replicas | `1` | +| `resources.requests.memory` | Memory request | `512Mi` | +| `resources.limits.memory` | Memory limit | `1Gi` | +| `nodeSelector` | Node selector labels | `{}` | +| `affinity` | Affinity rules | `{}` | +| `tolerations` | Tolerations | `[]` | +| `topologySpreadConstraints` | Topology spread constraints | `[]` | +| `strategy.type` | Deployment strategy | `Recreate` | + +### Migration + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `migration.enabled` | Enable pre-upgrade migration hook | `true` | +| `migration.image.repository` | Migration image (defaults to main image) | `""` | +| `migration.image.tag` | Migration image tag (defaults to main tag) | `""` | +| `migration.kubectl.image` | kubectl image for scale-down job | `bitnami/kubectl` | +| `migration.kubectl.tag` | kubectl image tag — defaults to `latest`; pin to a specific version compatible with your cluster (current Kubernetes version or n-1) when possible | `latest` | +| `migration.activeDeadlineSeconds` | Migration job deadline (seconds) | `600` | +| `migration.backoffLimit` | Job backoff limit | `0` | +| `migration.ttlSecondsAfterFinished` | Job TTL after completion | `604800` | +| `migration.backupPath` | Backup mount path | `/backup` | +| `migration.backupVolumeSizeLimit` | Backup volume size | `2Gi` | + +### Security + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `existingSecret` | Name of the Kubernetes secret | `devolutions-server` | +| `podSecurityContext.fsGroup` | Pod filesystem group | `1000` | +| `securityContext.runAsNonRoot` | Run as non-root | `true` | +| `securityContext.runAsUser` | Run as user ID | `1000` | +| `securityContext.runAsGroup` | Run as group ID | `1000` | +| `securityContext.allowPrivilegeEscalation` | Allow privilege escalation | `false` | +| `securityContext.capabilities.drop` | Linux capabilities to drop | `["ALL"]` | +| `securityContext.seccompProfile.type` | Seccomp profile type | `RuntimeDefault` | + +## Troubleshooting + +### Pod not starting + +Check the pod logs and events (adjust the `app` label and the `namespace` if you use overrides): + +```bash +kubectl logs -l app=devolutions-server -n devolutions-server +kubectl describe pod -l app=devolutions-server -n devolutions-server +``` + +### Migration job failing + +Not all upgrades trigger a database migration — they mostly occur on new major releases. When one does run, check the migration pod logs first: + +```bash +kubectl logs -l component=db-migration -n devolutions-server +``` + +Failed migration jobs are kept for debugging and cleaned up on the next upgrade or by the TTL controller. + +If you need to revert after a failed migration, restore the database from a snapshot taken before the upgrade **first**, then roll back to the previous image version. Rolling back the image without restoring the database will leave the schema in an inconsistent state. + +### TLS certificate issues + +Verify the certificate status: + +```bash +kubectl get certificate +kubectl describe certificate -tls +``` diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 0000000..6554129 --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,20 @@ +Devolutions Server has been deployed. + + Hostname: {{ include "devolutions-server.hostname" . }} + Namespace: {{ .Release.Namespace }} + +{{- if .Values.certificate.enabled }} + +A cert-manager Certificate resource has been created. +Check its status with: + + kubectl get certificate {{ include "devolutions-server.certificateName" . }} -n {{ .Release.Namespace }} +{{- end }} + +Check pod status: + + kubectl get pods -l {{ include "devolutions-server.kubectlSelector" . }} -n {{ .Release.Namespace }} + +View logs: + + kubectl logs -l {{ include "devolutions-server.kubectlSelector" . }} -n {{ .Release.Namespace }} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..720dd77 --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,136 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "devolutions-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "devolutions-server.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "devolutions-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "devolutions-server.labels" -}} +helm.sh/chart: {{ include "devolutions-server.chart" . }} +{{ include "devolutions-server.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "devolutions-server.selectorLabels" -}} +{{- if .Values.selectorLabels }} +{{- toYaml .Values.selectorLabels }} +{{- else }} +app: {{ include "devolutions-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +{{- end }} + +{{/* +Get the image tag to use +*/}} +{{- define "devolutions-server.imageTag" -}} +{{- .Values.image.tag | default .Chart.AppVersion }} +{{- end }} + +{{/* +Get the migration image repository +*/}} +{{- define "devolutions-server.migration.imageRepository" -}} +{{- .Values.migration.image.repository | default .Values.image.repository }} +{{- end }} + +{{/* +Get the migration image tag +*/}} +{{- define "devolutions-server.migration.imageTag" -}} +{{- .Values.migration.image.tag | default (include "devolutions-server.imageTag" .) }} +{{- end }} + +{{/* +Get the certificate secret name +*/}} +{{- define "devolutions-server.certificateSecretName" -}} +{{- .Values.certificate.secretName | default (printf "%s-tls" (include "devolutions-server.fullname" .)) }} +{{- end }} + +{{/* +Get the certificate resource name +Defaults to certificate.secretName if set, otherwise -tls +*/}} +{{- define "devolutions-server.certificateName" -}} +{{- .Values.certificate.name | default .Values.certificate.secretName | default (printf "%s-tls" (include "devolutions-server.fullname" .)) }} +{{- end }} + +{{/* +Get required DVLS hostname. +*/}} +{{- define "devolutions-server.hostname" -}} +{{- required "dvls.hostname is required" .Values.dvls.hostname }} +{{- end }} + +{{/* +Get required database host. +*/}} +{{- define "devolutions-server.databaseHost" -}} +{{- required "database.host is required" .Values.database.host }} +{{- end }} + +{{/* +Get required database name. +*/}} +{{- define "devolutions-server.databaseName" -}} +{{- required "database.name is required" .Values.database.name }} +{{- end }} + +{{/* +Selector labels formatted for kubectl -l flag. +*/}} +{{- define "devolutions-server.kubectlSelector" -}} +{{- if .Values.selectorLabels -}} +{{- $parts := list -}} +{{- range $k, $v := .Values.selectorLabels -}} +{{- $parts = append $parts (printf "%s=%s" $k $v) -}} +{{- end -}} +{{- join "," $parts -}} +{{- else -}} +app={{ include "devolutions-server.name" . }} +{{- end -}} +{{- end }} + +{{/* +Whether a TLS secret should be mounted. +*/}} +{{- define "devolutions-server.tlsSecretEnabled" -}} +{{- if or .Values.certificate.enabled .Values.certificate.secretName -}} +true +{{- end }} +{{- end }} diff --git a/chart/templates/backendtlspolicy.yaml b/chart/templates/backendtlspolicy.yaml new file mode 100644 index 0000000..568325d --- /dev/null +++ b/chart/templates/backendtlspolicy.yaml @@ -0,0 +1,21 @@ +{{- if .Values.backendTLSPolicy.enabled }} +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: {{ include "devolutions-server.fullname" . }} + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + targetRefs: + - group: "" + kind: Service + name: {{ include "devolutions-server.fullname" . }} + validation: + {{- if .Values.backendTLSPolicy.wellKnownCACertificates }} + wellKnownCACertificates: {{ .Values.backendTLSPolicy.wellKnownCACertificates }} + {{- else }} + caCertificateRefs: + {{- toYaml (required "backendTLSPolicy.caCertificateRefs is required when backendTLSPolicy.enabled is true and wellKnownCACertificates is not set" .Values.backendTLSPolicy.caCertificateRefs) | nindent 6 }} + {{- end }} + hostname: {{ include "devolutions-server.hostname" . }} +{{- end }} diff --git a/chart/templates/certificate.yaml b/chart/templates/certificate.yaml new file mode 100644 index 0000000..34c3ef6 --- /dev/null +++ b/chart/templates/certificate.yaml @@ -0,0 +1,17 @@ +{{- if .Values.certificate.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "devolutions-server.certificateName" . }} + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + dnsNames: + - {{ include "devolutions-server.hostname" . }} + issuerRef: + kind: {{ .Values.certificate.issuerKind }} + name: {{ required "certificate.issuerName is required when certificate.enabled is true" .Values.certificate.issuerName }} + secretName: {{ include "devolutions-server.certificateSecretName" . }} + privateKey: + rotationPolicy: {{ .Values.certificate.privateKeyRotationPolicy }} +{{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..c615920 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,179 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "devolutions-server.fullname" . }} + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: {{ .Values.strategy.type }} + selector: + matchLabels: + {{- include "devolutions-server.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "devolutions-server.selectorLabels" . | nindent 8 }} + spec: + automountServiceAccountToken: false + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ include "devolutions-server.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: {{ .Values.service.targetPort }} + name: https + protocol: TCP + env: + # DVLS Configuration + - name: DVLS_INIT + value: "false" + - name: DVLS_ADMIN_USERNAME + value: {{ .Values.dvls.admin.username | quote }} + - name: DVLS_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.dvls.admin.passwordSecretKey }} + - name: DVLS_ADMIN_EMAIL + value: {{ required "dvls.admin.email is required" .Values.dvls.admin.email | quote }} + - name: DVLS_PATH + value: {{ .Values.dvls.path | quote }} + - name: HOSTNAME + value: {{ include "devolutions-server.hostname" . | quote }} + + # Database Configuration + - name: {{ .Values.database.envPrefix }}_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.database.usernameSecretKey }} + - name: {{ .Values.database.envPrefix }}_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.database.passwordSecretKey }} + - name: {{ .Values.database.envPrefix }}_HOST + value: {{ include "devolutions-server.databaseHost" . | quote }} + - name: {{ .Values.database.envPrefix }}_NAME + value: {{ include "devolutions-server.databaseName" . | quote }} + - name: {{ .Values.database.envPrefix }}_PORT + value: {{ .Values.database.port | quote }} + + # Web Configuration + - name: WEB_SCHEME + value: {{ .Values.web.scheme | quote }} + - name: PORT + value: {{ .Values.web.port | quote }} + - name: EXTERNAL_WEB_SCHEME + value: {{ .Values.web.externalScheme | quote }} + - name: EXTERNAL_WEB_PORT + value: {{ .Values.web.externalPort | quote }} + + # ASP.NET Core Configuration + - name: ASPNETCORE_ENVIRONMENT + value: {{ .Values.aspnetcore.environment | quote }} + - name: ASPNETCORE_URLS + value: "{{ .Values.web.scheme }}://0.0.0.0:{{ .Values.web.port }}" + - name: DOTNET_RUNNING_IN_CONTAINER + value: "true" + - name: SCHEDULER_EMBEDDED + value: "true" + + # Security and Telemetry + - name: DVLS_TELEMETRY + value: {{ .Values.dvls.telemetry | quote }} + - name: DVLS_ENCRYPTION_CONFIG_B64 + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.dvls.encryptionConfigSecretKey }} + + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + # TLS Certificate Configuration (from cert-manager) + - name: TLS_CERTIFICATE_FILE + value: /etc/certs/tls.crt + - name: TLS_PRIVATE_KEY_FILE + value: /etc/certs/tls.key + {{- end }} + + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + volumeMounts: + - name: tls-certs + mountPath: /etc/certs + readOnly: true + {{- end }} + + livenessProbe: + httpGet: + path: /health + port: {{ .Values.service.targetPort }} + scheme: HTTPS + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + + readinessProbe: + httpGet: + path: /health + port: {{ .Values.service.targetPort }} + scheme: HTTPS + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + successThreshold: {{ .Values.probes.readiness.successThreshold }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + + startupProbe: + httpGet: + path: /health + port: {{ .Values.service.targetPort }} + scheme: HTTPS + initialDelaySeconds: {{ .Values.probes.startup.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.startup.periodSeconds }} + timeoutSeconds: {{ .Values.probes.startup.timeoutSeconds }} + failureThreshold: {{ .Values.probes.startup.failureThreshold }} + + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + volumes: + - name: tls-certs + secret: + secretName: {{ include "devolutions-server.certificateSecretName" . }} + defaultMode: 0444 + {{- end }} diff --git a/chart/templates/destinationrule.yaml b/chart/templates/destinationrule.yaml new file mode 100644 index 0000000..a7ae881 --- /dev/null +++ b/chart/templates/destinationrule.yaml @@ -0,0 +1,15 @@ +{{- if .Values.destinationRule.enabled }} +apiVersion: networking.istio.io/v1 +kind: DestinationRule +metadata: + name: {{ include "devolutions-server.fullname" . }}-tls + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + host: {{ include "devolutions-server.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + trafficPolicy: + tls: + mode: SIMPLE + caCertificates: /etc/ssl/certs/ca-certificates.crt + sni: {{ include "devolutions-server.hostname" . }} +{{- end }} diff --git a/chart/templates/hooks/db-migration-job.yaml b/chart/templates/hooks/db-migration-job.yaml new file mode 100644 index 0000000..364198a --- /dev/null +++ b/chart/templates/hooks/db-migration-job.yaml @@ -0,0 +1,134 @@ +{{- if .Values.migration.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "devolutions-server.fullname" . }}-db-migration + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} + component: db-migration + annotations: + # Helm hooks - run before upgrade + "helm.sh/hook": pre-upgrade + # Run migration as a pre-hook after RBAC and scale-down hooks + "helm.sh/hook-weight": "-5" + # Delete previous job before creating new one, cleanup on success + # Failed jobs are kept for debugging (cleaned up on next upgrade or by TTL) + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + parallelism: 1 + completions: 1 + activeDeadlineSeconds: {{ .Values.migration.activeDeadlineSeconds }} + backoffLimit: {{ .Values.migration.backoffLimit }} + ttlSecondsAfterFinished: {{ .Values.migration.ttlSecondsAfterFinished }} + template: + metadata: + labels: + {{- include "devolutions-server.selectorLabels" . | nindent 8 }} + component: db-migration + spec: + restartPolicy: Never + automountServiceAccountToken: false + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: db-migration + image: "{{ include "devolutions-server.migration.imageRepository" . }}:{{ include "devolutions-server.migration.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + # Database Configuration + - name: {{ .Values.database.envPrefix }}_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.database.usernameSecretKey }} + - name: {{ .Values.database.envPrefix }}_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.database.passwordSecretKey }} + - name: {{ .Values.database.envPrefix }}_HOST + value: {{ include "devolutions-server.databaseHost" . | quote }} + - name: {{ .Values.database.envPrefix }}_NAME + value: {{ include "devolutions-server.databaseName" . | quote }} + - name: {{ .Values.database.envPrefix }}_PORT + value: {{ .Values.database.port | quote }} + + # Migration Configuration + - name: DVLS_UPDATE_MODE + value: "true" + - name: DVLS_BACKUP_PATH + value: {{ .Values.migration.backupPath | quote }} + - name: DVLS_PATH + value: {{ .Values.dvls.path | quote }} + + # Web configuration (required by entrypoint) + - name: WEB_SCHEME + value: {{ .Values.web.scheme | quote }} + - name: PORT + value: {{ .Values.web.port | quote }} + - name: HOSTNAME + value: {{ include "devolutions-server.hostname" . | quote }} + + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + # TLS Certificate Configuration + - name: TLS_CERTIFICATE_FILE + value: /etc/certs/tls.crt + - name: TLS_PRIVATE_KEY_FILE + value: /etc/certs/tls.key + {{- end }} + + # Encryption Configuration + - name: DVLS_ENCRYPTION_CONFIG_B64 + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.dvls.encryptionConfigSecretKey }} + + volumeMounts: + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + - name: tls-certs + mountPath: /etc/certs + readOnly: true + {{- end }} + - name: dvls-backup + mountPath: {{ .Values.migration.backupPath }} + + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + + {{- with .Values.migration.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + + volumes: + {{- if (include "devolutions-server.tlsSecretEnabled" .) }} + - name: tls-certs + secret: + secretName: {{ include "devolutions-server.certificateSecretName" . }} + defaultMode: 0444 + {{- end }} + - name: dvls-backup + emptyDir: + sizeLimit: {{ .Values.migration.backupVolumeSizeLimit }} +{{- end }} diff --git a/chart/templates/hooks/scale-down-job.yaml b/chart/templates/hooks/scale-down-job.yaml new file mode 100644 index 0000000..c5ed39e --- /dev/null +++ b/chart/templates/hooks/scale-down-job.yaml @@ -0,0 +1,78 @@ +{{- if .Values.migration.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "devolutions-server.fullname" . }}-scale-down + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} + component: scale-down + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + activeDeadlineSeconds: 180 + backoffLimit: 1 + ttlSecondsAfterFinished: 300 + template: + metadata: + labels: + {{- include "devolutions-server.selectorLabels" . | nindent 8 }} + component: scale-down + spec: + serviceAccountName: {{ include "devolutions-server.fullname" . }}-deployer + restartPolicy: Never + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: scale-down + image: "{{ .Values.migration.kubectl.image }}:{{ .Values.migration.kubectl.tag }}" + command: + - /bin/sh + - -c + - | + DEPLOY_NAME="{{ include "devolutions-server.fullname" . }}" + NAMESPACE="{{ .Release.Namespace }}" + echo "Scaling down deployment ${DEPLOY_NAME}..." + OUTPUT=$(kubectl get deployment "${DEPLOY_NAME}" -n "${NAMESPACE}" 2>&1) + EXIT_CODE=$? + if [ ${EXIT_CODE} -eq 0 ]; then + kubectl scale deployment/"${DEPLOY_NAME}" --replicas=0 -n "${NAMESPACE}" + echo "Waiting for rollout to complete..." + kubectl rollout status deployment/"${DEPLOY_NAME}" -n "${NAMESPACE}" --timeout=120s + echo "Scale down complete" + elif echo "${OUTPUT}" | grep -q "NotFound"; then + echo "Deployment does not exist yet, skipping scale down" + else + echo "Failed to query deployment: ${OUTPUT}" >&2 + exit 1 + fi + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 64Mi + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} +{{- end }} diff --git a/chart/templates/hooks/scale-down-rbac.yaml b/chart/templates/hooks/scale-down-rbac.yaml new file mode 100644 index 0000000..c341d7e --- /dev/null +++ b/chart/templates/hooks/scale-down-rbac.yaml @@ -0,0 +1,53 @@ +{{- if .Values.migration.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "devolutions-server.fullname" . }}-deployer + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-15" + "helm.sh/hook-delete-policy": before-hook-creation +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "devolutions-server.fullname" . }}-deployer + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-15" + "helm.sh/hook-delete-policy": before-hook-creation +rules: + - apiGroups: ["apps"] + resources: ["deployments", "deployments/scale"] + resourceNames: ["{{ include "devolutions-server.fullname" . }}"] + verbs: ["get", "patch", "update"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["list", "watch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "devolutions-server.fullname" . }}-deployer + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade + "helm.sh/hook-weight": "-15" + "helm.sh/hook-delete-policy": before-hook-creation +subjects: + - kind: ServiceAccount + name: {{ include "devolutions-server.fullname" . }}-deployer + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: {{ include "devolutions-server.fullname" . }}-deployer + apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/chart/templates/httproute.yaml b/chart/templates/httproute.yaml new file mode 100644 index 0000000..7ae913d --- /dev/null +++ b/chart/templates/httproute.yaml @@ -0,0 +1,26 @@ +{{- if .Values.httproute.enabled }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "devolutions-server.fullname" . }}-https + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + parentRefs: + - name: {{ .Values.httproute.gateway.name }} + namespace: {{ .Values.httproute.gateway.namespace }} + {{- if .Values.httproute.gateway.sectionName }} + sectionName: {{ .Values.httproute.gateway.sectionName }} + {{- end }} + hostnames: + - {{ include "devolutions-server.hostname" . }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "devolutions-server.fullname" . }} + port: {{ .Values.service.port }} + weight: 100 +{{- end }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..dfcb60c --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "devolutions-server.fullname" . }} + labels: + {{- include "devolutions-server.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + protocol: TCP + name: https + appProtocol: https + selector: + {{- include "devolutions-server.selectorLabels" . | nindent 4 }} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..477f97a --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,203 @@ +# Default values for Devolutions Server +# This is a YAML-formatted file. + +replicaCount: 1 + +image: + repository: devolutions/devolutions-server + # Overrides the image tag whose default is the chart appVersion. + tag: '' + pullPolicy: IfNotPresent + +# Image pull secrets for private registries +imagePullSecrets: [] + +nameOverride: '' +fullnameOverride: '' + +# Override selector labels used by Deployment, Service, and hooks. +# Useful for migrating from an existing release with different labels. +# Example: +# selectorLabels: +# app: dvls +selectorLabels: {} + +# DVLS Configuration +dvls: + # External hostname for DVLS (required) + hostname: '' + # Admin credentials + admin: + username: dvls-admin + email: '' + # Key in existingSecret containing the admin password + passwordSecretKey: dvls-admin-password + # Path inside container + path: /opt/devolutions/dvls + # Telemetry + telemetry: false + # Key in existingSecret containing the base64-encoded encryption config + encryptionConfigSecretKey: dvls-encryption-config + +# Database configuration +# The DVLS container supports both DATABASE_* and AZURE_SQL_* environment variable +# prefixes (see entrypoint.ps1). You can set database.usernameSecretKey and +# database.passwordSecretKey to match whichever convention your secret uses. +database: + # SQL Server hostname (required) + host: '' + # Database name (required) + name: '' + port: '1433' + # Environment variable prefix for database credentials (DATABASE or AZURE_SQL) + envPrefix: DATABASE + # Keys in existingSecret containing database credentials + usernameSecretKey: db-username + passwordSecretKey: db-password + +# ASP.NET Core configuration +aspnetcore: + environment: Production + +# Web/HTTP configuration +web: + scheme: https + port: '5000' + externalScheme: https + externalPort: '443' + +# TLS Certificate configuration +certificate: + # Enable cert-manager Certificate resource + enabled: true + # Certificate resource name (defaults to -tls) + name: '' + # Secret name for TLS certificate (defaults to -tls) + secretName: '' + # ClusterIssuer name (required when certificate.enabled is true) + issuerName: '' + issuerKind: ClusterIssuer + # Private key rotation policy + privateKeyRotationPolicy: Always + +# Service configuration +service: + type: ClusterIP + port: 5000 + targetPort: 5000 + +# HTTPRoute (Gateway API) configuration +httproute: + enabled: false + gateway: + name: '' + namespace: '' + sectionName: '' + +# BackendTLSPolicy (Gateway API) - enables TLS from gateway to backend +backendTLSPolicy: + enabled: false + # Use well-known CA certificates (e.g., "System") instead of explicit refs + wellKnownCACertificates: '' + # CA certificate references for backend TLS validation + # Required when enabled and wellKnownCACertificates is not set + # Example: + # - name: my-ca + # group: "" + # kind: ConfigMap + caCertificateRefs: [] + +# Istio DestinationRule +destinationRule: + enabled: false + +# Resource limits +resources: + requests: + memory: 512Mi + limits: + memory: 1Gi + +# Node selector +nodeSelector: {} + +# Affinity rules +affinity: {} + +# Tolerations +tolerations: [] + +# Topology spread constraints +topologySpreadConstraints: [] + +# Pod security context +podSecurityContext: + fsGroup: 1000 + +# Container security context +securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +# Probes configuration +probes: + liveness: + initialDelaySeconds: 180 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readiness: + initialDelaySeconds: 120 + periodSeconds: 15 + timeoutSeconds: 8 + successThreshold: 1 + failureThreshold: 3 + startup: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 8 + failureThreshold: 60 + +# Deployment strategy +strategy: + type: Recreate + +# Database Migration Job configuration +migration: + # Enable migration job as pre-upgrade hook + enabled: true + # Image override (defaults to same as main image) + image: + repository: '' # If empty, uses main image repository + tag: '' # If empty, uses main image tag + # kubectl image used by the scale-down hook job + kubectl: + image: bitnami/kubectl + tag: latest + # Job configuration + activeDeadlineSeconds: 600 + backoffLimit: 0 + ttlSecondsAfterFinished: 604800 + # Backup path inside container + backupPath: /backup + # Backup volume size limit (adjust based on database size) + backupVolumeSizeLimit: 2Gi + # Resource limits for migration + resources: + requests: + memory: 512Mi + cpu: 250m + limits: + memory: 1Gi + +# Name of an existing Kubernetes Secret containing all credentials. +# Required keys: dvls-admin-password, dvls-encryption-config, +# and the keys specified by database.usernameSecretKey / database.passwordSecretKey. +existingSecret: devolutions-server