diff --git a/.github/actions/setup-helm-repos/action.yml b/.github/actions/setup-helm-repos/action.yml new file mode 100644 index 000000000..0ea44f837 --- /dev/null +++ b/.github/actions/setup-helm-repos/action.yml @@ -0,0 +1,12 @@ +name: 'Setup Helm Repositories' +description: 'Add required Helm repositories for STAC FastAPI' + +runs: + using: 'composite' + steps: + - name: Add Helm repositories + shell: bash + run: | + helm repo add elasticsearch https://helm.elastic.co + helm repo add opensearch https://opensearch-project.github.io/helm-charts/ + helm repo update \ No newline at end of file diff --git a/.github/ct-lint.yaml b/.github/ct-lint.yaml new file mode 100644 index 000000000..befb4dc7e --- /dev/null +++ b/.github/ct-lint.yaml @@ -0,0 +1,43 @@ +# Chart Testing Lint Configuration +# Additional linting rules for chart-testing + +rules: + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + colons: + max-spaces-before: 0 + max-spaces-after: 1 + commas: + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: + min-spaces-from-content: 1 + comments-indentation: disable + document-end: disable + document-start: disable + empty-lines: + max: 2 + max-start: 0 + max-end: 1 + hyphens: + max-spaces-after: 1 + indentation: + spaces: 2 + indent-sequences: true + key-duplicates: enable + line-length: + max: 120 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + new-line-at-end-of-file: enable + octal-values: disable + quoted-strings: disable + trailing-spaces: enable + truthy: + allowed-values: ['true', 'false'] + check-keys: false \ No newline at end of file diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 000000000..c49b55b29 --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,33 @@ +# Chart Testing Configuration +# This configuration is used by the chart-testing (ct) tool + +# Chart directories relative to repository root +chart-dirs: + - helm-chart + +# Target branch for pull request testing +target-branch: main + +# Chart repositories to add +chart-repos: + - elasticsearch=https://helm.elastic.co + - opensearch=https://opensearch-project.github.io/helm-charts/ + +# Helm version to use +helm-version: v3.13.0 + +# Additional configurations +validate-chart-schema: true +validate-maintainers: false +check-version-increment: true +process-skipped-charts: false + +# Test configuration +upgrade: false +skip-missing-values: true + +# Linting configuration +lint-conf: .github/ct-lint.yaml + +# Additional Helm lint settings +helm-lint-extra-args: --set app.image.tag=latest \ No newline at end of file diff --git a/.github/workflows/helm-chart-test.yml b/.github/workflows/helm-chart-test.yml new file mode 100644 index 000000000..53142457a --- /dev/null +++ b/.github/workflows/helm-chart-test.yml @@ -0,0 +1,199 @@ +name: Helm Chart Tests + +on: + push: + branches: [ main, develop, feat/* ] + paths: + - 'helm-chart/**' + - '.github/workflows/helm-chart-test.yml' + pull_request: + branches: [ main, develop ] + paths: + - 'helm-chart/**' + - '.github/workflows/helm-chart-test.yml' + +env: + HELM_VERSION: v3.13.0 + KUBECTL_VERSION: v1.28.0 + KIND_VERSION: v0.20.0 + +jobs: + lint: + name: Lint Helm Chart + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: ${{ env.HELM_VERSION }} + + - name: Setup Helm repositories + uses: ./.github/actions/setup-helm-repos + + - name: Lint Helm chart + run: | + cd helm-chart/stac-fastapi + helm dependency update + helm lint . + + test-matrix: + name: Test Chart + runs-on: ubuntu-latest + needs: lint + strategy: + matrix: + backend: [elasticsearch, opensearch] + kubernetes-version: [v1.27.3, v1.28.0] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: ${{ env.HELM_VERSION }} + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: ${{ env.KUBECTL_VERSION }} + + - name: Set up Kind + uses: helm/kind-action@v1.8.0 + with: + version: ${{ env.KIND_VERSION }} + node_image: kindest/node:${{ matrix.kubernetes-version }} + cluster_name: stac-fastapi-test + + - name: Setup Helm repositories + uses: ./.github/actions/setup-helm-repos + + - name: Run matrix tests + env: + BACKEND: ${{ matrix.backend }} + MATRIX_MODE: true + run: | + chmod +x ./helm-chart/test-chart.sh + ./helm-chart/test-chart.sh -m -b ${{ matrix.backend }} ci + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-report-${{ matrix.backend }}-k8s-${{ matrix.kubernetes-version }} + path: test-report-*.json + + integration-test: + name: Integration Tests + runs-on: ubuntu-latest + needs: test-matrix + if: github.event_name == 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: ${{ env.HELM_VERSION }} + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: ${{ env.KUBECTL_VERSION }} + + - name: Set up Kind + uses: helm/kind-action@v1.8.0 + with: + version: ${{ env.KIND_VERSION }} + cluster_name: stac-fastapi-integration + + - name: Setup Helm repositories + uses: ./.github/actions/setup-helm-repos + + - name: Run full integration tests + run: | + chmod +x ./helm-chart/test-chart.sh + ./helm-chart/test-chart.sh test-all + + - name: Test upgrade scenarios + run: | + # Test elasticsearch to opensearch migration scenario + ./helm-chart/test-chart.sh -b elasticsearch install + ./helm-chart/test-chart.sh validate + ./helm-chart/test-chart.sh cleanup + + # Test opensearch deployment + ./helm-chart/test-chart.sh -b opensearch install + ./helm-chart/test-chart.sh validate + ./helm-chart/test-chart.sh cleanup + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: ${{ env.HELM_VERSION }} + + - name: Setup Helm repositories + uses: ./.github/actions/setup-helm-repos + + - name: Run Checkov security scan + uses: bridgecrewio/checkov-action@master + with: + directory: helm-chart/ + framework: kubernetes + output_format: sarif + output_file_path: reports/results.sarif + + - name: Upload Checkov results + if: always() + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: reports/results.sarif + + chart-testing: + name: Chart Testing (ct) + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Helm + uses: azure/setup-helm@v3 + with: + version: ${{ env.HELM_VERSION }} + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.1 + + - name: Setup Helm repositories + uses: ./.github/actions/setup-helm-repos + + - name: Run chart-testing (lint) + run: ct lint --config .github/ct.yaml + + - name: Set up Kind cluster + uses: helm/kind-action@v1.8.0 + with: + version: ${{ env.KIND_VERSION }} + + - name: Run chart-testing (install) + run: ct install --config .github/ct.yaml \ No newline at end of file diff --git a/.gitignore b/.gitignore index c1213091f..496f61d85 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,14 @@ venv /docs/src/api/* .DS_Store + +# Helm +*.tgz +charts/*/charts/ +charts/*/requirements.lock +charts/*/Chart.lock +helm-chart/stac-fastapi/charts/ +helm-chart/stac-fastapi/Chart.lock +helm-chart/stac-fastapi/*.tgz +helm-chart/test-results/ +helm-chart/tmp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e867050b4..65cd67583 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,4 +41,20 @@ repos: #args: [ # Don't require docstrings for tests #'--match=(?!test|alembic|scripts).*\.py', - #] \ No newline at end of file + #] + - repo: local + hooks: + - id: helm-chart-lint + name: Helm chart lint + language: docker_image + entry: quay.io/helmpack/chart-testing:v3.7.1 + args: + - bash + - -c + - >- + export HELM_CONFIG_HOME=/tmp/helm && + export HELM_DATA_HOME=/tmp/helm && + export HELM_CACHE_HOME=/tmp/helm && + ct lint --config /src/.github/ct.yaml + pass_filenames: false + files: ^helm-chart/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6323533ec..164c7f21d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Introduced SFEOS Tools (`sfeos_tools/`) - An installable Click-based CLI package for managing SFEOS deployments. Initial command `add-bbox-shape` adds the `bbox_shape` field to existing collections for spatial search compatibility. Install with `pip install sfeos-tools[elasticsearch]` or `pip install sfeos-tools[opensearch]`. [#481](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/481) - CloudFerro logo to sponsors and supporters list [#485](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/485) - Latest news section to README [#485](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/485) +- Added Helm chart for ES or OS in-cluster deployment [#455](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/455) ### Changed diff --git a/helm-chart/stac-fastapi/Chart.yaml b/helm-chart/stac-fastapi/Chart.yaml new file mode 100644 index 000000000..c635dfa29 --- /dev/null +++ b/helm-chart/stac-fastapi/Chart.yaml @@ -0,0 +1,32 @@ +apiVersion: v2 +name: stac-fastapi +description: A Helm chart for STAC FastAPI with Elasticsearch and OpenSearch backends +type: application +version: 0.1.0 +appVersion: "6.3.0" + +keywords: + - stac + - fastapi + - elasticsearch + - opensearch + - geospatial + - api + +home: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch +sources: + - https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch + +maintainers: + - name: STAC Utils + url: https://github.com/stac-utils + +dependencies: + - name: elasticsearch + version: 8.5.1 + repository: https://helm.elastic.co + condition: elasticsearch.enabled + - name: opensearch + version: 3.2.1 + repository: https://opensearch-project.github.io/helm-charts/ + condition: opensearch.enabled diff --git a/helm-chart/stac-fastapi/README.md b/helm-chart/stac-fastapi/README.md new file mode 100644 index 000000000..8694f7027 --- /dev/null +++ b/helm-chart/stac-fastapi/README.md @@ -0,0 +1,495 @@ +# STAC FastAPI Helm Chart + +This Helm chart deploys the STAC FastAPI application with support for both Elasticsearch and OpenSearch backends. + +## Overview + +The chart provides a flexible deployment solution for STAC FastAPI with the following features: + +- **Dual Backend Support**: Choose between Elasticsearch or OpenSearch +- **Bundled or External Database**: Deploy with bundled database or connect to external clusters +- **Production Ready**: Includes monitoring, autoscaling, and security configurations +- **High Availability**: Support for multi-replica deployments with proper disruption budgets +- **Performance Optimized**: Configurable performance settings for large-scale deployments + +## Prerequisites + +- Kubernetes 1.16+ +- Helm 3.0+ +- Storage class for persistent volumes (if using bundled databases) + +## Quick Start + +### Deploy with Elasticsearch + +```bash +# Add dependencies +helm dependency update ./helm-chart/stac-fastapi + +# Install with Elasticsearch backend +helm install my-stac-api ./helm-chart/stac-fastapi \ + --set backend=elasticsearch \ + --set elasticsearch.enabled=true \ + --set opensearch.enabled=false +``` + +### Deploy with OpenSearch + +```bash +# Add dependencies +helm dependency update ./helm-chart/stac-fastapi + +# Install with OpenSearch backend +helm install my-stac-api ./helm-chart/stac-fastapi \ + --set backend=opensearch \ + --set elasticsearch.enabled=false \ + --set opensearch.enabled=true +``` + +### Deploy with External Database + +```bash +# Create secret for API key (if needed) +kubectl create secret generic elasticsearch-api-key \ + --from-literal=api-key="your-api-key-here" + +# Install with external database +helm install my-stac-api ./helm-chart/stac-fastapi \ + --values ./helm-chart/stac-fastapi/values-external.yaml \ + --set externalDatabase.host="your-database-host" \ + --set externalDatabase.port=9200 +``` + +## Configuration + +### Global Options + +```yaml +global: + imageRegistry: "" + storageClass: "" + clusterDomain: "cluster.local" +``` + +The chart builds fully qualified service endpoints for bundled databases using the Kubernetes cluster domain. Adjust `clusterDomain` if your cluster doesn't use the default `cluster.local` suffix. + +## Backend Selection + +The chart supports both Elasticsearch and OpenSearch backends, but only deploys **one backend at a time** based on the `backend` configuration: + +### Elasticsearch Backend + +```yaml +backend: elasticsearch +elasticsearch: + enabled: true +opensearch: + enabled: false +``` + +### OpenSearch Backend + +```yaml +backend: opensearch +elasticsearch: + enabled: false +opensearch: + enabled: true +``` + +### How It Works + +1. **Chart Dependencies**: Both Elasticsearch and OpenSearch charts are listed as dependencies with conditions +2. **Conditional Deployment**: Only the backend specified by `backend` value is enabled +3. **Resource Isolation**: When deploying with elasticsearch, no OpenSearch resources are created (and vice versa) +4. **Automatic Configuration**: The application automatically connects to the correct backend service + +### Values Files + +Use the provided values files for easy backend selection: + +- **Elasticsearch**: `helm install stac-fastapi ./stac-fastapi -f values-elasticsearch.yaml` +- **OpenSearch**: `helm install stac-fastapi ./stac-fastapi -f values-opensearch.yaml` + +This ensures efficient resource usage and prevents conflicts between backends. + +### Application Configuration + +Key application settings: + +```yaml +app: + replicaCount: 2 + + image: + repository: ghcr.io/stac-utils/stac-fastapi + tag: "latest" + pullPolicy: IfNotPresent + + waitForDatabase: + enabled: true + intervalSeconds: 2 + maxAttempts: 120 + + env: + STAC_FASTAPI_TITLE: "STAC API" + STAC_FASTAPI_DESCRIPTION: "A STAC FastAPI implementation" + ENVIRONMENT: "production" + WEB_CONCURRENCY: "10" + ENABLE_DIRECT_RESPONSE: "false" + DATABASE_REFRESH: "false" + ENABLE_DATETIME_INDEX_FILTERING: "false" + STAC_FASTAPI_RATE_LIMIT: "200/minute" +``` + +The optional `waitForDatabase` block adds a lightweight init container that blocks STAC FastAPI startup until the backing Elasticsearch/OpenSearch service is reachable—mirroring the docker-compose `wait-for-it` helper. Disable it by setting `app.waitForDatabase.enabled=false` if you prefer the application to start immediately and rely on internal retries instead. + +### Database Configuration + +#### Application Credentials + +If your Elasticsearch or OpenSearch cluster requires authentication, provide credentials to the application with the `app.databaseAuth` block. You can reference an existing secret or supply literal values: + +```yaml +app: + databaseAuth: + existingSecret: "stac-opensearch-admin" # Optional. When set, keys are read from this secret. + usernameKey: "username" # Secret key that stores the username (defaults to "username"). + passwordKey: "password" # Secret key that stores the password (defaults to "password"). + # username: "admin" # Optional literal username when not using a secret. + # password: "changeme" # Optional literal password when not using a secret. +``` + +#### Bundled Elasticsearch + +```yaml +elasticsearch: + enabled: true + clusterName: "stac-elasticsearch" + replicas: 3 + minimumMasterNodes: 2 + + resources: + requests: + cpu: "1000m" + memory: "4Gi" + limits: + cpu: "2000m" + memory: "4Gi" + + volumeClaimTemplate: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 100Gi +``` + +#### Bundled OpenSearch + +```yaml +opensearch: + enabled: true + clusterName: "stac-opensearch" + replicas: 3 + + resources: + requests: + cpu: "1000m" + memory: "4Gi" + limits: + cpu: "2000m" + memory: "4Gi" + + persistence: + enabled: true + size: 100Gi +``` + +#### External Database + +```yaml +externalDatabase: + enabled: true + host: "elasticsearch.example.com" + port: 9200 + ssl: true + verifyCerts: true + apiKeySecret: "elasticsearch-credentials" + apiKeySecretKey: "api-key" +``` + +### Ingress Configuration + +```yaml +app: + ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: stac-api.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: stac-fastapi-tls + hosts: + - stac-api.example.com +``` + +### Autoscaling + +```yaml +app: + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 +``` + +### Monitoring + +```yaml +monitoring: + enabled: true + prometheus: + enabled: true + serviceMonitor: + enabled: true + interval: 30s + scrapeTimeout: 10s +``` + +## Performance Tuning + +### For Large Datasets + +Enable datetime-based index filtering for better performance with temporal queries: + +```yaml +app: + env: + ENABLE_DATETIME_INDEX_FILTERING: "true" + DATETIME_INDEX_MAX_SIZE_GB: "50" +``` + +### For Maximum Performance + +Enable direct response mode (disables FastAPI dependencies): + +```yaml +app: + env: + ENABLE_DIRECT_RESPONSE: "true" + DATABASE_REFRESH: "false" +``` + +### For High Throughput + +Increase worker processes and rate limits: + +```yaml +app: + env: + WEB_CONCURRENCY: "8" + STAC_FASTAPI_RATE_LIMIT: "1000/minute" +``` + +## Security + +### Network Policies + +Enable network policies for additional security: + +```yaml +networkPolicy: + enabled: true + allowNamespaceCommunication: true # allow pods in the release namespace to talk to each other + allowDNS: true # keep kube-dns reachable for service discovery + ingress: + - from: + - namespaceSelector: + matchLabels: + name: nginx-ingress + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: opensearch + ports: + - protocol: TCP + port: 9200 +``` + +### OpenSearch Admin Credentials + +When deploying with the OpenSearch backend you can instruct the chart to generate +an initial admin password and store it in a Kubernetes secret. Enable this by +setting `opensearchSecurity.generateAdminPassword=true` (already enabled in +`values-opensearch.yaml`). The chart will create a secret named +`-stac-fastapi-opensearch-admin` by default and automatically wires it to +the STAC FastAPI deployment through environment variables. + +Retrieve the generated credentials with: + +```bash +kubectl get secret -stac-fastapi-opensearch-admin \ + -o jsonpath='{.data.username}' | base64 --decode +kubectl get secret -stac-fastapi-opensearch-admin \ + -o jsonpath='{.data.password}' | base64 --decode +``` + +You can provide your own secret name, username key, or password key through the +`opensearchSecurity` values block if you already manage credentials externally. + +### Pod Security Context + +Configure security contexts: + +```yaml +app: + podSecurityContext: + fsGroup: 2000 + + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 +``` + +## High Availability + +### Pod Disruption Budget + +Ensure availability during maintenance: + +```yaml +podDisruptionBudget: + enabled: true + minAvailable: 1 +``` + +### Anti-Affinity + +Spread pods across nodes: + +```yaml +app: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - stac-fastapi + topologyKey: kubernetes.io/hostname +``` + +## Examples + +The chart includes several example values files: + +- `values-elasticsearch.yaml`: Production-ready Elasticsearch deployment +- `values-opensearch.yaml`: Production-ready OpenSearch deployment +- `values-external.yaml`: External database configuration + +Use them as starting points: + +```bash +helm install my-stac-api ./helm-chart/stac-fastapi \ + --values ./helm-chart/stac-fastapi/values-elasticsearch.yaml +``` + +## Upgrading + +To upgrade an existing deployment: + +```bash +helm upgrade my-stac-api ./helm-chart/stac-fastapi \ + --values your-values.yaml +``` + +## Uninstalling + +To remove the deployment: + +```bash +helm uninstall my-stac-api +``` + +**Note**: Persistent volumes for databases may need to be manually deleted. + +## Troubleshooting + +### Check Pod Status + +```bash +kubectl get pods -l app.kubernetes.io/name=stac-fastapi +``` + +### View Logs + +```bash +kubectl logs -l app.kubernetes.io/name=stac-fastapi +``` + +### Check Database Connectivity + +```bash +kubectl exec -it deployment/my-stac-api -- curl http://elasticsearch:9200/_health +``` + +### Port Forward for Local Testing + +```bash +kubectl port-forward service/my-stac-api 8080:80 +``` + +Then visit + +## Configuration Reference + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `backend` | Database backend (elasticsearch/opensearch) | `elasticsearch` | +| `app.replicaCount` | Number of application replicas | `2` | +| `app.image.repository` | Application image repository | `ghcr.io/stac-utils/stac-fastapi` | +| `app.image.tag` | Application image tag | `""` (uses chart appVersion) | +| `app.service.type` | Kubernetes service type | `ClusterIP` | +| `app.service.port` | Service port | `80` | +| `app.ingress.enabled` | Enable ingress | `false` | +| `app.autoscaling.enabled` | Enable horizontal pod autoscaler | `false` | +| `elasticsearch.enabled` | Deploy Elasticsearch | `true` | +| `opensearch.enabled` | Deploy OpenSearch | `false` | +| `externalDatabase.enabled` | Use external database | `false` | +| `opensearchSecurity.generateAdminPassword` | Generate random OpenSearch admin password | `false` | +| `opensearchSecurity.secretName` | Override name of generated OpenSearch admin secret | `""` | +| `monitoring.enabled` | Enable monitoring | `false` | +| `networkPolicy.enabled` | Enable network policies | `false` | +| `networkPolicy.allowNamespaceCommunication` | Allow ingress/egress within the release namespace | `true` | +| `networkPolicy.allowDNS` | Allow egress to kube-dns for service discovery | `true` | +| `podDisruptionBudget.enabled` | Enable pod disruption budget | `false` | + +For a complete list of configuration options, see the `values.yaml` file. + +## Contributing + +Contributions are welcome! Please ensure any changes maintain compatibility with both Elasticsearch and OpenSearch backends. + +## License + +This chart is released under the same license as the STAC FastAPI project. diff --git a/helm-chart/stac-fastapi/templates/NOTES.txt b/helm-chart/stac-fastapi/templates/NOTES.txt new file mode 100644 index 000000000..9314411c6 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/NOTES.txt @@ -0,0 +1,84 @@ +1. Get the application URL by running these commands: +{{- if .Values.app.ingress.enabled }} +{{- range $host := .Values.app.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.app.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.app.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stac-fastapi.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.app.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "stac-fastapi.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stac-fastapi.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.app.service.port }} +{{- else if contains "ClusterIP" .Values.app.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "{{ include "stac-fastapi.selectorLabels" . }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} + +2. Backend Configuration: + - Backend: {{ .Values.backend }} + {{- if .Values.externalDatabase.enabled }} + - Database: External ({{ .Values.externalDatabase.host }}:{{ .Values.externalDatabase.port }}) + {{- else if eq .Values.backend "elasticsearch" }} + - Database: Bundled Elasticsearch + {{- else if eq .Values.backend "opensearch" }} + - Database: Bundled OpenSearch + {{- end }} + +3. API Documentation: + Once the application is running, you can access: + - OpenAPI documentation: /docs + - STAC API landing page: / + - Collections endpoint: /collections + - Search endpoint: /search + +4. Health Checks: + - Liveness probe: GET / + - Readiness probe: GET / + +{{- if and (eq .Values.backend "elasticsearch") .Values.elasticsearch.enabled }} + +5. Elasticsearch Access: + export ES_POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ .Release.Name }}-elasticsearch-master" -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ .Release.Namespace }} port-forward $ES_POD_NAME 9200:9200 + # Then access Elasticsearch at http://localhost:9200 +{{- end }} + +{{- if and (eq .Values.backend "opensearch") .Values.opensearch.enabled }} + +5. OpenSearch Access: + export OS_POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name=opensearch" -o jsonpath="{.items[0].metadata.name}") + kubectl --namespace {{ .Release.Namespace }} port-forward $OS_POD_NAME 9200:9200 + # Then access OpenSearch at http://localhost:9200 +{{- end }} + +{{- if .Values.app.autoscaling.enabled }} + +6. Autoscaling: + Horizontal Pod Autoscaler is enabled with: + - Min replicas: {{ .Values.app.autoscaling.minReplicas }} + - Max replicas: {{ .Values.app.autoscaling.maxReplicas }} + {{- if .Values.app.autoscaling.targetCPUUtilizationPercentage }} + - CPU target: {{ .Values.app.autoscaling.targetCPUUtilizationPercentage }}% + {{- end }} + {{- if .Values.app.autoscaling.targetMemoryUtilizationPercentage }} + - Memory target: {{ .Values.app.autoscaling.targetMemoryUtilizationPercentage }}% + {{- end }} +{{- end }} + +{{- if .Values.monitoring.enabled }} + +7. Monitoring: + {{- if .Values.monitoring.prometheus.enabled }} + - Prometheus metrics: /metrics + {{- if .Values.monitoring.prometheus.serviceMonitor.enabled }} + - ServiceMonitor created for Prometheus Operator + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/_helpers.tpl b/helm-chart/stac-fastapi/templates/_helpers.tpl new file mode 100644 index 000000000..cc5daae3c --- /dev/null +++ b/helm-chart/stac-fastapi/templates/_helpers.tpl @@ -0,0 +1,306 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "stac-fastapi.name" -}} +{{- default .Chart.Name .Values.app.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 "stac-fastapi.fullname" -}} +{{- if .Values.app.fullnameOverride }} +{{- .Values.app.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.app.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 "stac-fastapi.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "stac-fastapi.labels" -}} +helm.sh/chart: {{ include "stac-fastapi.chart" . }} +{{ include "stac-fastapi.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "stac-fastapi.selectorLabels" -}} +app.kubernetes.io/name: {{ include "stac-fastapi.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "stac-fastapi.serviceAccountName" -}} +{{- if .Values.app.serviceAccount.create }} +{{- default (include "stac-fastapi.fullname" .) .Values.app.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.app.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Name for the autogenerated OpenSearch admin secret. +*/}} +{{- define "stac-fastapi.opensearchAdminSecretName" -}} +{{- if .Values.opensearchSecurity.secretName }} +{{- .Values.opensearchSecurity.secretName }} +{{- else }} +{{- printf "%s-opensearch-admin" (include "stac-fastapi.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Mutate values to ensure OpenSearch initial admin credentials are wired when +password generation is enabled. +*/}} +{{- define "stac-fastapi.configureOpensearchSecurity" -}} +{{- if and (eq .Values.backend "opensearch") (.Values.opensearch.enabled) (.Values.opensearchSecurity.generateAdminPassword | default false) }} + {{- $secretName := include "stac-fastapi.opensearchAdminSecretName" . -}} + {{- $passwordKey := default "password" .Values.opensearchSecurity.passwordKey -}} + {{- $usernameKey := default "username" .Values.opensearchSecurity.usernameKey -}} + {{- $flags := dict "hasPassword" false "hasUsername" false -}} + {{- range $env := (default (list) .Values.opensearch.extraEnvs) }} + {{- if eq (get $env "name") "OPENSEARCH_INITIAL_ADMIN_PASSWORD" }} + {{- $_ := set $flags "hasPassword" true -}} + {{- end }} + {{- if eq (get $env "name") "OPENSEARCH_INITIAL_ADMIN_USERNAME" }} + {{- $_ := set $flags "hasUsername" true -}} + {{- end }} + {{- end }} + {{- if not (get $flags "hasPassword") }} + {{- $passwordEnv := dict "name" "OPENSEARCH_INITIAL_ADMIN_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" $secretName "key" $passwordKey)) -}} + {{- $_ := set .Values.opensearch "extraEnvs" (append (default (list) .Values.opensearch.extraEnvs) $passwordEnv) -}} + {{- end }} + {{- if not (get $flags "hasUsername") }} + {{- $usernameEnv := dict "name" "OPENSEARCH_INITIAL_ADMIN_USERNAME" "valueFrom" (dict "secretKeyRef" (dict "name" $secretName "key" $usernameKey)) -}} + {{- $_ := set .Values.opensearch "extraEnvs" (append (default (list) .Values.opensearch.extraEnvs) $usernameEnv) -}} + {{- end }} +{{- end }} +{{- end }} + +{{/* +Create the bundled database service name based on backend selection +*/}} +{{- define "stac-fastapi.databaseServiceName" -}} +{{- if eq .Values.backend "elasticsearch" }} + {{- if .Values.elasticsearch.enabled }} + {{- if .Values.elasticsearch.masterService }} + {{- .Values.elasticsearch.masterService }} + {{- else if .Values.elasticsearch.fullnameOverride }} + {{- printf "%s-master" .Values.elasticsearch.fullnameOverride }} + {{- else if .Values.elasticsearch.clusterName }} + {{- printf "%s-master" .Values.elasticsearch.clusterName }} + {{- else }} + {{- printf "%s-%s" .Release.Name "elasticsearch-master" }} + {{- end }} + {{- else }} + {{- fail "Elasticsearch is not enabled but backend is set to elasticsearch" }} + {{- end }} +{{- else if eq .Values.backend "opensearch" }} + {{- if .Values.opensearch.enabled }} + {{- if .Values.opensearch.masterService }} + {{- .Values.opensearch.masterService }} + {{- else if .Values.opensearch.fullnameOverride }} + {{- printf "%s-master" .Values.opensearch.fullnameOverride }} + {{- else if .Values.opensearch.clusterName }} + {{- printf "%s-master" .Values.opensearch.clusterName }} + {{- else }} + {{- printf "%s-%s" .Release.Name "opensearch-cluster-master" }} + {{- end }} + {{- else }} + {{- fail "OpenSearch is not enabled but backend is set to opensearch" }} + {{- end }} +{{- else }} + {{- fail "Invalid backend specified. Must be 'elasticsearch' or 'opensearch'" }} +{{- end }} +{{- end }} + +{{/* +Create the database host based on backend selection +*/}} +{{- define "stac-fastapi.databaseHost" -}} +{{- if .Values.externalDatabase.enabled }} + {{- .Values.externalDatabase.host }} +{{- else }} + {{- $service := include "stac-fastapi.databaseServiceName" . | trim }} + {{- $namespace := .Release.Namespace | default "default" }} + {{- $clusterDomain := default "cluster.local" .Values.global.clusterDomain }} + {{- printf "%s.%s.svc.%s" $service $namespace $clusterDomain }} +{{- end }} +{{- end }} + +{{/* +Create the database port based on backend selection +*/}} +{{- define "stac-fastapi.databasePort" -}} +{{- if .Values.externalDatabase.enabled }} +{{- .Values.externalDatabase.port }} +{{- else if eq .Values.backend "elasticsearch" }} +{{- .Values.elasticsearch.service.httpPort | default 9200 }} +{{- else if eq .Values.backend "opensearch" }} +{{- .Values.opensearch.service.httpPort | default 9200 }} +{{- end }} +{{- end }} + +{{/* +Create the image repository with tag +*/}} +{{- define "stac-fastapi.image" -}} +{{- $registry := .Values.global.imageRegistry | default .Values.app.image.repository }} +{{- $tag := .Values.app.image.tag | default .Chart.AppVersion }} +{{- if eq .Values.backend "elasticsearch" }} +{{- printf "%s-es:%s" $registry $tag }} +{{- else if eq .Values.backend "opensearch" }} +{{- printf "%s-os:%s" $registry $tag }} +{{- end }} +{{- end }} + +{{/* +Create environment variables for the application +*/}} +{{- define "stac-fastapi.environment" -}} +{{- $env := default (dict) .Values.app.env -}} +{{- $envFromSecret := default (dict) .Values.app.envFromSecret -}} +{{- $dbAuth := deepCopy (default (dict) .Values.app.databaseAuth) -}} +{{- $opensearchSecurity := default (dict) .Values.opensearchSecurity -}} +{{- if and (eq .Values.backend "opensearch") (get $opensearchSecurity "generateAdminPassword") }} + {{- $secretName := include "stac-fastapi.opensearchAdminSecretName" . -}} + {{- if or (not (hasKey $dbAuth "existingSecret")) (not $dbAuth.existingSecret) }} + {{- $_ := set $dbAuth "existingSecret" $secretName -}} + {{- end }} + {{- if not (hasKey $dbAuth "usernameKey") }} + {{- $_ := set $dbAuth "usernameKey" (default "username" (get $opensearchSecurity "usernameKey")) -}} + {{- end }} + {{- if not (hasKey $dbAuth "passwordKey") }} + {{- $_ := set $dbAuth "passwordKey" (default "password" (get $opensearchSecurity "passwordKey")) -}} + {{- end }} +{{- end }} +- name: BACKEND + value: {{ .Values.backend | quote }} +- name: ES_HOST + value: {{ include "stac-fastapi.databaseHost" . | quote }} +- name: ES_PORT + value: {{ include "stac-fastapi.databasePort" . | quote }} +{{- if $dbAuth.existingSecret }} +{{- $usernameKey := default "username" $dbAuth.usernameKey }} +{{- $passwordKey := default "password" $dbAuth.passwordKey }} +{{- if not (hasKey $env "ES_USER") }} +- name: ES_USER + valueFrom: + secretKeyRef: + name: {{ $dbAuth.existingSecret }} + key: {{ $usernameKey }} +{{- end }} +{{- if not (hasKey $env "ES_PASS") }} +- name: ES_PASS + valueFrom: + secretKeyRef: + name: {{ $dbAuth.existingSecret }} + key: {{ $passwordKey }} +{{- end }} +{{- else }} +{{- if and (not (hasKey $env "ES_USER")) $dbAuth.username }} +- name: ES_USER + value: {{ $dbAuth.username | quote }} +{{- end }} +{{- if and (not (hasKey $env "ES_PASS")) $dbAuth.password }} +- name: ES_PASS + value: {{ $dbAuth.password | quote }} +{{- end }} +{{- end }} +{{- if .Values.externalDatabase.enabled }} +- name: ES_USE_SSL + value: {{ .Values.externalDatabase.ssl | quote }} +- name: ES_VERIFY_CERTS + value: {{ .Values.externalDatabase.verifyCerts | quote }} +- name: ES_TIMEOUT + value: {{ .Values.externalDatabase.timeout | quote }} +{{- if .Values.externalDatabase.apiKeySecret }} +- name: ES_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalDatabase.apiKeySecret }} + key: {{ .Values.externalDatabase.apiKeySecretKey }} +{{- end }} +{{- end }} +{{- range $key, $value := $env }} +- name: {{ $key }} + value: {{ $value | quote }} +{{- end }} +{{- range $key, $secretName := $envFromSecret }} +- name: {{ $key }} + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $key }} +{{- end }} +{{- end }} + +{{/* +Determine if Elasticsearch should be enabled based on backend selection +*/}} +{{- define "stac-fastapi.elasticsearch.enabled" -}} +{{- if eq .Values.backend "elasticsearch" }} +{{- true }} +{{- else }} +{{- false }} +{{- end }} +{{- end }} + +{{/* +Determine if OpenSearch should be enabled based on backend selection +*/}} +{{- define "stac-fastapi.opensearch.enabled" -}} +{{- if eq .Values.backend "opensearch" }} +{{- true }} +{{- else }} +{{- false }} +{{- end }} +{{- end }} + +{{/* +Determine PodDisruptionBudget apiVersion based on cluster capabilities +*/}} +{{- define "stac-fastapi.pdb.apiVersion" -}} +{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion }} +policy/v1 +{{- else }} +policy/v1beta1 +{{- end }} +{{- end }} + +{{/* +Determine HorizontalPodAutoscaler apiVersion based on cluster capabilities +*/}} +{{- define "stac-fastapi.hpa.apiVersion" -}} +{{- if .Capabilities.APIVersions.Has "autoscaling/v2" }} +autoscaling/v2 +{{- else if .Capabilities.APIVersions.Has "autoscaling/v2beta2" }} +autoscaling/v2beta2 +{{- else }} +autoscaling/v2beta1 +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/deployment.yaml b/helm-chart/stac-fastapi/templates/deployment.yaml new file mode 100644 index 000000000..c246cc891 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/deployment.yaml @@ -0,0 +1,92 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} +spec: + {{- if not .Values.app.autoscaling.enabled }} + replicas: {{ .Values.app.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "stac-fastapi.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.app.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "stac-fastapi.selectorLabels" . | nindent 8 }} + spec: + {{- if .Values.app.waitForDatabase.enabled }} + initContainers: + - name: wait-for-database + image: {{ .Values.app.waitForDatabase.image | default "busybox:1.36.1" }} + imagePullPolicy: {{ .Values.app.waitForDatabase.pullPolicy | default "IfNotPresent" }} + command: + - sh + - -c + - | + set -e + HOST="{{ include "stac-fastapi.databaseHost" . | trim }}" + PORT="{{ include "stac-fastapi.databasePort" . | trim }}" + ATTEMPTS={{ .Values.app.waitForDatabase.maxAttempts | default 120 }} + INTERVAL={{ .Values.app.waitForDatabase.intervalSeconds | default 2 }} + COUNT=0 + echo "Waiting for ${HOST}:${PORT} before starting STAC FastAPI..." + while true; do + if nc -z "$HOST" "$PORT"; then + echo "${HOST}:${PORT} is reachable." + exit 0 + fi + COUNT=$((COUNT + 1)) + if [ "$COUNT" -ge "$ATTEMPTS" ]; then + echo "Timed out waiting for ${HOST}:${PORT} after ${COUNT} attempts." >&2 + exit 1 + fi + sleep "$INTERVAL" + done + {{- with .Values.app.waitForDatabase.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + {{- with .Values.app.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "stac-fastapi.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.app.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.app.securityContext | nindent 12 }} + image: {{ include "stac-fastapi.image" . }} + imagePullPolicy: {{ .Values.app.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.app.service.targetPort }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.app.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.app.readinessProbe | nindent 12 }} + env: + {{- include "stac-fastapi.environment" . | nindent 12 }} + resources: + {{- toYaml .Values.app.resources | nindent 12 }} + {{- with .Values.app.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.app.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.app.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/hpa.yaml b/helm-chart/stac-fastapi/templates/hpa.yaml new file mode 100644 index 000000000..eb233e12d --- /dev/null +++ b/helm-chart/stac-fastapi/templates/hpa.yaml @@ -0,0 +1,41 @@ +{{- if .Values.app.autoscaling.enabled }} +{{- $hpaApiVersion := (include "stac-fastapi.hpa.apiVersion" . | trim) }} +apiVersion: {{ $hpaApiVersion }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "stac-fastapi.fullname" . }} + minReplicas: {{ .Values.app.autoscaling.minReplicas }} + maxReplicas: {{ .Values.app.autoscaling.maxReplicas }} + metrics: + {{- if .Values.app.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + {{- if or (eq $hpaApiVersion "autoscaling/v2") (eq $hpaApiVersion "autoscaling/v2beta2") }} + target: + type: Utilization + averageUtilization: {{ .Values.app.autoscaling.targetCPUUtilizationPercentage }} + {{- else }} + targetAverageUtilization: {{ .Values.app.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- end }} + {{- if .Values.app.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + {{- if or (eq $hpaApiVersion "autoscaling/v2") (eq $hpaApiVersion "autoscaling/v2beta2") }} + target: + type: Utilization + averageUtilization: {{ .Values.app.autoscaling.targetMemoryUtilizationPercentage }} + {{- else }} + targetAverageUtilization: {{ .Values.app.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/ingress.yaml b/helm-chart/stac-fastapi/templates/ingress.yaml new file mode 100644 index 000000000..3a31feb3d --- /dev/null +++ b/helm-chart/stac-fastapi/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- if .Values.app.ingress.enabled -}} +{{- $fullName := include "stac-fastapi.fullname" . -}} +{{- $svcPort := .Values.app.service.port -}} +{{- if and .Values.app.ingress.className (not (hasKey .Values.app.ingress.annotations "kubernetes.io/ingress.class")) }} + {{- $_ := set .Values.app.ingress.annotations "kubernetes.io/ingress.class" .Values.app.ingress.className}} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} + {{- with .Values.app.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.app.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.app.ingress.className }} + {{- end }} + {{- if .Values.app.ingress.tls }} + tls: + {{- range .Values.app.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.app.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/networkpolicy.yaml b/helm-chart/stac-fastapi/templates/networkpolicy.yaml new file mode 100644 index 000000000..d83c19ba1 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/networkpolicy.yaml @@ -0,0 +1,48 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} +{{- $ingressRules := list }} +{{- if .Values.networkPolicy.allowNamespaceCommunication }} + {{- $ingressRules = append $ingressRules (dict "from" (list (dict "podSelector" (dict)))) }} +{{- end }} +{{- range $_, $rule := .Values.networkPolicy.ingress }} + {{- $ingressRules = append $ingressRules $rule }} +{{- end }} +{{- $egressRules := list }} +{{- if .Values.networkPolicy.allowNamespaceCommunication }} + {{- $egressRules = append $egressRules (dict "to" (list (dict "podSelector" (dict)))) }} +{{- end }} +{{- if .Values.networkPolicy.allowDNS }} + {{- $dnsTo := dict "namespaceSelector" (dict "matchLabels" (dict "kubernetes.io/metadata.name" "kube-system")) "podSelector" (dict "matchLabels" (dict "k8s-app" "kube-dns")) }} + {{- $dnsPorts := list (dict "protocol" "UDP" "port" 53) (dict "protocol" "TCP" "port" 53) }} + {{- $egressRules = append $egressRules (dict "to" (list $dnsTo) "ports" $dnsPorts) }} +{{- end }} +{{- range $_, $rule := .Values.networkPolicy.egress }} + {{- $egressRules = append $egressRules $rule }} +{{- end }} +{{- $hasIngress := gt (len $ingressRules) 0 }} +{{- $hasEgress := gt (len $egressRules) 0 }} +spec: + podSelector: + matchLabels: + {{- include "stac-fastapi.selectorLabels" . | nindent 6 }} + policyTypes: + {{- if $hasIngress }} + - Ingress + {{- end }} + {{- if $hasEgress }} + - Egress + {{- end }} + {{- if $hasIngress }} + ingress: + {{- toYaml $ingressRules | nindent 4 }} + {{- end }} + {{- if $hasEgress }} + egress: + {{- toYaml $egressRules | nindent 4 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/opensearch-admin-secret.yaml b/helm-chart/stac-fastapi/templates/opensearch-admin-secret.yaml new file mode 100644 index 000000000..e7abc0082 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/opensearch-admin-secret.yaml @@ -0,0 +1,30 @@ +{{- if and (eq .Values.backend "opensearch") (.Values.opensearchSecurity.generateAdminPassword | default false) }} +{{- include "stac-fastapi.configureOpensearchSecurity" . -}} +{{- $secretName := include "stac-fastapi.opensearchAdminSecretName" . -}} +{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}} +{{- $usernameKey := default "username" .Values.opensearchSecurity.usernameKey -}} +{{- $passwordKey := default "password" .Values.opensearchSecurity.passwordKey -}} +{{- $username := default "admin" .Values.opensearchSecurity.username -}} +{{- $passwordLength := int (default 32 .Values.opensearchSecurity.passwordLength) -}} +{{- $passwordB64 := "" -}} +{{- if and $existing (hasKey $existing.data $passwordKey) }} + {{- $passwordB64 = index $existing.data $passwordKey -}} +{{- else }} + {{- $passwordB64 = randAlphaNum $passwordLength | b64enc -}} +{{- end }} +{{- $usernameB64 := b64enc $username -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} + {{- with .Values.opensearchSecurity.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +type: Opaque +data: + {{ $usernameKey }}: {{ $usernameB64 }} + {{ $passwordKey }}: {{ $passwordB64 }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/opensearch-security-config.yaml b/helm-chart/stac-fastapi/templates/opensearch-security-config.yaml new file mode 100644 index 000000000..2ea17d26c --- /dev/null +++ b/helm-chart/stac-fastapi/templates/opensearch-security-config.yaml @@ -0,0 +1,11 @@ +{{- /* + This template doesn't create resources; it mutates values so the + OpenSearch subchart receives the generated admin credentials. +*/ -}} +{{- include "stac-fastapi.configureOpensearchSecurity" . -}} +{{- if (and (eq .Values.backend "opensearch") (.Values.opensearch.enabled) (.Values.opensearchSecurity.generateAdminPassword | default false)) }} +# debug-extraEnvs-count: {{ len (default (list) .Values.opensearch.extraEnvs) }} +{{- range $env := (default (list) .Values.opensearch.extraEnvs) }} +# debug-extraEnv: {{ toYaml $env | replace "\n" " " | trim }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/poddisruptionbudget.yaml b/helm-chart/stac-fastapi/templates/poddisruptionbudget.yaml new file mode 100644 index 000000000..b8f510362 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/poddisruptionbudget.yaml @@ -0,0 +1,19 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- $pdbApiVersion := (include "stac-fastapi.pdb.apiVersion" . | trim) }} +apiVersion: {{ $pdbApiVersion }} +kind: PodDisruptionBudget +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} +spec: + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: + {{- include "stac-fastapi.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/service.yaml b/helm-chart/stac-fastapi/templates/service.yaml new file mode 100644 index 000000000..c44d3fee5 --- /dev/null +++ b/helm-chart/stac-fastapi/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} +spec: + type: {{ .Values.app.service.type }} + ports: + - port: {{ .Values.app.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "stac-fastapi.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/serviceaccount.yaml b/helm-chart/stac-fastapi/templates/serviceaccount.yaml new file mode 100644 index 000000000..bf517ca9c --- /dev/null +++ b/helm-chart/stac-fastapi/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "stac-fastapi.serviceAccountName" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} + {{- with .Values.app.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/templates/servicemonitor.yaml b/helm-chart/stac-fastapi/templates/servicemonitor.yaml new file mode 100644 index 000000000..f4da1405c --- /dev/null +++ b/helm-chart/stac-fastapi/templates/servicemonitor.yaml @@ -0,0 +1,20 @@ +{{- if and .Values.monitoring.enabled .Values.monitoring.prometheus.enabled .Values.monitoring.prometheus.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "stac-fastapi.fullname" . }} + labels: + {{- include "stac-fastapi.labels" . | nindent 4 }} + {{- with .Values.monitoring.prometheus.serviceMonitor.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "stac-fastapi.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + interval: {{ .Values.monitoring.prometheus.serviceMonitor.interval }} + scrapeTimeout: {{ .Values.monitoring.prometheus.serviceMonitor.scrapeTimeout }} + path: /metrics +{{- end }} \ No newline at end of file diff --git a/helm-chart/stac-fastapi/values-elasticsearch.yaml b/helm-chart/stac-fastapi/values-elasticsearch.yaml new file mode 100644 index 000000000..cd2f5e897 --- /dev/null +++ b/helm-chart/stac-fastapi/values-elasticsearch.yaml @@ -0,0 +1,170 @@ +# Example values for deploying STAC FastAPI with Elasticsearch backend +# Override the default values.yaml with these settings + +# Set backend to elasticsearch +backend: elasticsearch + +# STAC FastAPI application configuration +app: + replicaCount: 2 + + image: + repository: ghcr.io/stac-utils/stac-fastapi + tag: "latest" + pullPolicy: IfNotPresent + + # Service configuration + service: + type: LoadBalancer + port: 80 + targetPort: 8080 + + waitForDatabase: + enabled: true + intervalSeconds: 2 + maxAttempts: 180 + + # Ingress configuration + ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: stac-api.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: stac-fastapi-tls + hosts: + - stac-api.example.com + + # Resource limits + resources: + limits: + cpu: 1000m + memory: 2Gi + requests: + cpu: 500m + memory: 1Gi + + # Autoscaling + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + + # Environment variables + env: + STAC_FASTAPI_TITLE: "Production STAC API with Elasticsearch" + STAC_FASTAPI_DESCRIPTION: "High-performance STAC API for geospatial data discovery" + ENVIRONMENT: "production" + WEB_CONCURRENCY: "4" + ENABLE_DIRECT_RESPONSE: "true" # Enable for maximum performance + DATABASE_REFRESH: "false" # Better performance for bulk operations + ENABLE_DATETIME_INDEX_FILTERING: "true" # Enable for large datasets + DATETIME_INDEX_MAX_SIZE_GB: "50" + STAC_FASTAPI_RATE_LIMIT: "1000/minute" + ES_USE_SSL: "true" + ES_VERIFY_CERTS: "false" + + databaseAuth: + existingSecret: "stac-elasticsearch-prod-master-credentials" + usernameKey: "username" + passwordKey: "password" + +# Elasticsearch configuration +elasticsearch: + enabled: true + + # Cluster configuration + clusterName: "stac-elasticsearch-prod" + replicas: 3 + minimumMasterNodes: 2 + + # Resource configuration for production + resources: + requests: + cpu: "1000m" + memory: "4Gi" + limits: + cpu: "2000m" + memory: "4Gi" + + # JVM heap size (should be 50% of memory limit) + esJavaOpts: "-Xmx2g -Xms2g" + + # Production-ready Elasticsearch configuration + esConfig: + elasticsearch.yml: | + cluster.name: "stac-elasticsearch-prod" + network.host: 0.0.0.0 + discovery.seed_hosts: ["stac-elasticsearch-prod-master-headless"] + cluster.initial_master_nodes: ["stac-elasticsearch-prod-master-0", "stac-elasticsearch-prod-master-1", "stac-elasticsearch-prod-master-2"] + action.destructive_requires_name: true + xpack.security.enabled: false + xpack.security.transport.ssl.enabled: false + xpack.security.http.ssl.enabled: false + indices.memory.index_buffer_size: 20% + thread_pool.write.queue_size: 1000 + thread_pool.search.queue_size: 1000 + xpack.security.http.ssl.client_authentication: optional + + # Persistent storage + volumeClaimTemplate: + accessModes: ["ReadWriteOnce"] + # storageClassName: "fast-ssd" # Use high-performance storage class + resources: + requests: + storage: 100Gi + +# Disable OpenSearch since we're using Elasticsearch +opensearch: + enabled: false + +# External database disabled since we're using bundled Elasticsearch +externalDatabase: + enabled: false + +# Monitoring configuration +monitoring: + enabled: true + prometheus: + enabled: true + serviceMonitor: + enabled: true + interval: 30s + scrapeTimeout: 10s + labels: + monitoring: "prometheus" + +# Pod disruption budget for high availability +podDisruptionBudget: + enabled: true + minAvailable: 1 + +# Network policies for security +networkPolicy: + enabled: true + allowNamespaceCommunication: true + allowDNS: true + ingress: + - from: + - namespaceSelector: + matchLabels: + name: nginx-ingress + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - podSelector: + matchLabels: + app: stac-elasticsearch-prod-master + ports: + - protocol: TCP + port: 9200 \ No newline at end of file diff --git a/helm-chart/stac-fastapi/values-opensearch.yaml b/helm-chart/stac-fastapi/values-opensearch.yaml new file mode 100644 index 000000000..942933bbf --- /dev/null +++ b/helm-chart/stac-fastapi/values-opensearch.yaml @@ -0,0 +1,205 @@ +# Example values for deploying STAC FastAPI with OpenSearch backend +# Override the default values.yaml with these settings + +# Set backend to opensearch +backend: opensearch + +# STAC FastAPI application configuration +app: + replicaCount: 2 + + image: + repository: ghcr.io/stac-utils/stac-fastapi + tag: "latest" + pullPolicy: IfNotPresent + + # Service configuration + service: + type: LoadBalancer + port: 80 + targetPort: 8080 + + waitForDatabase: + enabled: true + intervalSeconds: 2 + maxAttempts: 180 + + # Ingress configuration + ingress: + enabled: true + className: "nginx" + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + cert-manager.io/cluster-issuer: "letsencrypt-prod" + hosts: + - host: stac-api-os.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: stac-fastapi-os-tls + hosts: + - stac-api-os.example.com + + # Resource limits + resources: + limits: + cpu: 1000m + memory: 2Gi + requests: + cpu: 500m + memory: 1Gi + + # Autoscaling + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + targetMemoryUtilizationPercentage: 80 + + # Environment variables + env: + STAC_FASTAPI_TITLE: "Production STAC API with OpenSearch" + STAC_FASTAPI_DESCRIPTION: "High-performance STAC API for geospatial data discovery with OpenSearch" + ENVIRONMENT: "production" + WEB_CONCURRENCY: "4" + ENABLE_DIRECT_RESPONSE: "true" # Enable for maximum performance + DATABASE_REFRESH: "false" # Better performance for bulk operations + ENABLE_DATETIME_INDEX_FILTERING: "true" # Enable for large datasets + DATETIME_INDEX_MAX_SIZE_GB: "50" + STAC_FASTAPI_RATE_LIMIT: "1000/minute" + ES_USE_SSL: "true" + ES_VERIFY_CERTS: "false" + + # Optional: pull connection credentials from a secret managed by the + # OpenSearch chart (or provide plain values via username/password). + databaseAuth: + existingSecret: "" + usernameKey: "username" + passwordKey: "password" + +# Enable automatic OpenSearch admin password generation +opensearchSecurity: + generateAdminPassword: true + # Use a deterministic name so the generated secret can be referenced below + secretName: stac-opensearch-admin + usernameKey: username + passwordKey: password + +# Disable Elasticsearch since we're using OpenSearch +elasticsearch: + enabled: false + +# OpenSearch configuration +opensearch: + enabled: true + + # Cluster configuration + clusterName: "stac-opensearch-prod" + masterService: "opensearch-cluster-master" + replicas: 3 + + # OpenSearch roles for each node + roles: + - master + - ingest + - data + - remote_cluster_client + + # Resource configuration for production + resources: + requests: + cpu: "1000m" + memory: "4Gi" + limits: + cpu: "2000m" + memory: "4Gi" + + # JVM heap size (should be 50% of memory limit) + opensearchJavaOpts: "-Xmx2g -Xms2g" + + # Production-ready OpenSearch configuration + config: + opensearch.yml: | + cluster.name: stac-opensearch-prod + network.host: 0.0.0.0 + discovery.seed_hosts: ["opensearch-cluster-master-headless"] + cluster.initial_cluster_manager_nodes: ["stac-opensearch-prod-master-0", "stac-opensearch-prod-master-1", "stac-opensearch-prod-master-2"] + action.destructive_requires_name: true + indices.memory.index_buffer_size: 20% + thread_pool.write.queue_size: 1000 + thread_pool.search.queue_size: 1000 + cluster.routing.allocation.disk.threshold_enabled: true + cluster.routing.allocation.disk.watermark.low: 85% + cluster.routing.allocation.disk.watermark.high: 90% + cluster.routing.allocation.disk.watermark.flood_stage: 95% + + # Extra environment variables + extraEnvs: + - name: "DISABLE_INSTALL_DEMO_CONFIG" + value: "false" + - name: "DISABLE_SECURITY_PLUGIN" + value: "false" + - name: "OPENSEARCH_INITIAL_ADMIN_USERNAME" + valueFrom: + secretKeyRef: + name: stac-opensearch-admin + key: username + - name: "OPENSEARCH_INITIAL_ADMIN_PASSWORD" + valueFrom: + secretKeyRef: + name: stac-opensearch-admin + key: password + + # Persistent storage + persistence: + enabled: true + # storageClass: "fast-ssd" # Use high-performance storage class + accessModes: + - ReadWriteOnce + size: 100Gi + annotations: {} + +# External database disabled since we're using bundled OpenSearch +externalDatabase: + enabled: false + +# Monitoring configuration +monitoring: + enabled: true + prometheus: + enabled: true + serviceMonitor: + enabled: true + interval: 30s + scrapeTimeout: 10s + labels: + monitoring: "prometheus" + +# Pod disruption budget for high availability +podDisruptionBudget: + enabled: true + minAvailable: 1 + +# Network policies for security +networkPolicy: + enabled: true + allowNamespaceCommunication: true + allowDNS: true + ingress: + - from: + - namespaceSelector: + matchLabels: + name: nginx-ingress + ports: + - protocol: TCP + port: 8080 + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: opensearch + ports: + - protocol: TCP + port: 9200 \ No newline at end of file diff --git a/helm-chart/stac-fastapi/values.yaml b/helm-chart/stac-fastapi/values.yaml new file mode 100644 index 000000000..9744caf36 --- /dev/null +++ b/helm-chart/stac-fastapi/values.yaml @@ -0,0 +1,399 @@ +# Default values for stac-fastapi. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# Backend configuration - choose 'elasticsearch' or 'opensearch' +backend: elasticsearch + +# Global configuration +global: + imageRegistry: "" + storageClass: "" + clusterDomain: "cluster.local" + +# STAC FastAPI application configuration +app: + # Number of replicas for the application + replicaCount: 2 + + image: + # Repository for the STAC FastAPI image + repository: ghcr.io/stac-utils/stac-fastapi + # Image pull policy + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion + tag: "" + + # Image pull secrets + imagePullSecrets: [] + + # Override the name of the chart + nameOverride: "" + # Override the full name of the chart + fullnameOverride: "" + + # Service account configuration + serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + + # Pod annotations + podAnnotations: {} + + # Pod security context + podSecurityContext: {} + # fsGroup: 2000 + + # Security context + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + + # Service configuration + service: + type: ClusterIP + port: 80 + targetPort: 8080 + + # Wait for bundled database before starting the app (similar to docker-compose setup) + waitForDatabase: + enabled: true + image: busybox:1.36.1 + pullPolicy: IfNotPresent + intervalSeconds: 2 + maxAttempts: 120 + resources: {} + + # Ingress configuration + ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: stac-fastapi.local + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: stac-fastapi-tls + # hosts: + # - stac-fastapi.local + + # Resource limits and requests + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 1Gi + + # Horizontal Pod Autoscaler + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + + # Node selector + nodeSelector: {} + + # Tolerations + tolerations: [] + + # Affinity + affinity: {} + + # Liveness probe configuration + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + + # Readiness probe configuration + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + + # Environment variables for STAC FastAPI + env: + # Application configuration + STAC_FASTAPI_TITLE: "stac-fastapi" + STAC_FASTAPI_DESCRIPTION: "A STAC FastAPI with configurable backend" + STAC_FASTAPI_VERSION: "6.0.0" + STAC_FASTAPI_LANDING_PAGE_ID: "stac-fastapi" + APP_HOST: "0.0.0.0" + APP_PORT: "8080" + ENVIRONMENT: "production" + WEB_CONCURRENCY: "10" + RELOAD: "false" + + # Database configuration + DATABASE_REFRESH: "false" + ENABLE_TRANSACTIONS_EXTENSIONS: "true" + STAC_ITEM_LIMIT: "10" + ENV_MAX_LIMIT: "10000" + STAC_INDEX_ASSETS: "false" + RAISE_ON_BULK_ERROR: "false" + + # Performance configuration + ENABLE_DIRECT_RESPONSE: "false" + + # Rate limiting + STAC_FASTAPI_RATE_LIMIT: "200/minute" + + # Datetime index filtering + ENABLE_DATETIME_INDEX_FILTERING: "false" + DATETIME_INDEX_MAX_SIZE_GB: "25" + STAC_ITEMS_INDEX_PREFIX: "items_" + + # SSL configuration + ES_USE_SSL: "false" + ES_VERIFY_CERTS: "false" + + # Additional environment variables from secrets + envFromSecret: {} + # ES_API_KEY: "es-api-key-secret" + + # Database authentication configuration for bundled backends + databaseAuth: + # Plain username to connect to Elasticsearch/OpenSearch. Leave empty to skip. + username: "" + # Plain password to connect to Elasticsearch/OpenSearch. Leave empty to skip. + password: "" + # Reference an existing secret containing credentials. When set, the + # username/password keys below will be looked up from this secret instead of + # using the plain values above. + existingSecret: "" + # Secret key that stores the username. Defaults to "username" when empty. + usernameKey: "username" + # Secret key that stores the password. Defaults to "password" when empty. + passwordKey: "password" + +# OpenSearch security helper +opensearchSecurity: + generateAdminPassword: false + username: "admin" + usernameKey: "username" + passwordKey: "password" + passwordLength: 32 + secretName: "" + annotations: {} + +# Elasticsearch configuration +# Note: This will be automatically set based on 'backend' value +elasticsearch: + # Enable/disable Elasticsearch deployment + enabled: true + + # Elasticsearch cluster name + clusterName: "stac-elasticsearch" + + # Node group configuration + nodeGroup: "master" + + # Number of masters eligible nodes + masterService: "stac-elasticsearch-master" + + # Elasticsearch roles + roles: + - master + - ingest + - data + - remote_cluster_client + + # Number of replicas + replicas: 1 + + # Minimum master nodes + minimumMasterNodes: 1 + + # Elasticsearch configuration + esConfig: + elasticsearch.yml: | + cluster.name: "stac-elasticsearch" + network.host: 0.0.0.0 + discovery.type: single-node + action.destructive_requires_name: false + xpack.security.enabled: false + xpack.security.transport.ssl.enabled: false + xpack.security.http.ssl.enabled: false + + # JVM configuration + esJavaOpts: "-Xmx1g -Xms1g" + + # Resource configuration + resources: + requests: + cpu: "100m" + memory: "2Gi" + limits: + cpu: "1000m" + memory: "2Gi" + + # Volume claim template + volumeClaimTemplate: + accessModes: ["ReadWriteOnce"] + # storageClassName: "" # Leave empty to use cluster default + resources: + requests: + storage: 1Gi + + # Service configuration + service: + type: ClusterIP + nodePort: "" + annotations: {} + httpPortName: http + transportPortName: transport + labels: {} + labelsHeadless: {} + httpPort: 9200 + transportPort: 9300 + +# OpenSearch configuration +opensearch: + # Enable/disable OpenSearch deployment + enabled: false + + # OpenSearch cluster name + clusterName: "stac-opensearch" + + # Service name used to expose the master nodes + masterService: "opensearch-cluster-master" + + # Node group configuration + nodeGroup: "master" + + # OpenSearch roles + roles: + - master + - ingest + - data + - remote_cluster_client + + # Number of replicas + replicas: 1 + + # Minimum master nodes + majorVersion: "" + + # OpenSearch configuration + config: + opensearch.yml: | + cluster.name: stac-opensearch + network.host: 0.0.0.0 + discovery.type: single-node + plugins.security.disabled: true + action.destructive_requires_name: false + + # JVM configuration + opensearchJavaOpts: "-Xmx1g -Xms1g" + + # Resource configuration + resources: + requests: + cpu: "100m" + memory: "2Gi" + limits: + cpu: "1000m" + memory: "2Gi" + + # Persistence configuration + persistence: + enabled: true + storageClass: "" + accessModes: + - ReadWriteOnce + size: 10Gi + annotations: {} + + # Service configuration + service: + type: ClusterIP + nodePort: "" + annotations: {} + httpPortName: http + transportPortName: transport + labels: {} + httpPort: 9200 + transportPort: 9300 + +# External database configuration (when not using bundled ES/OS) +externalDatabase: + # Whether to use external database instead of bundled one + enabled: false + # Database host + host: "" + # Database port + port: 9200 + # Whether to use SSL + ssl: false + # Whether to verify certificates + verifyCerts: false + # Connection timeout + timeout: 30 + # API key for authentication (reference to secret) + apiKeySecret: "" + # API key field in the secret + apiKeySecretKey: "api-key" + +# Configuration for monitoring and observability +monitoring: + # Enable monitoring + enabled: false + + # Prometheus monitoring + prometheus: + enabled: false + serviceMonitor: + enabled: false + interval: 30s + scrapeTimeout: 10s + labels: {} + + # Grafana dashboards + grafana: + enabled: false + dashboards: {} + +# Network policies +networkPolicy: + enabled: false + allowNamespaceCommunication: true + allowDNS: true + ingress: [] + egress: [] + +# Pod disruption budget +podDisruptionBudget: + enabled: false + minAvailable: 1 + # maxUnavailable: 1 + +# Security policies +securityPolicy: + enabled: false + psp: + enabled: false diff --git a/helm-chart/test-chart.sh b/helm-chart/test-chart.sh new file mode 100755 index 000000000..2494cabda --- /dev/null +++ b/helm-chart/test-chart.sh @@ -0,0 +1,794 @@ +#!/bin/bash + +# STAC FastAPI Helm Chart Testing Script +# This script helps validate and test Helm chart deployments + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +CHART_PATH="./helm-chart/stac-fastapi" +RELEASE_NAME="stac-fastapi-test" +NAMESPACE="stac-fastapi" +BACKEND=${BACKEND:-"elasticsearch"} +MATRIX_MODE=${MATRIX_MODE:-false} + +# Functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Help function +show_help() { + cat << EOF +STAC FastAPI Helm Chart Testing Script + +Usage: $0 [OPTIONS] COMMAND + +Commands: + lint Lint the Helm chart + test Run Helm chart tests + test-all Test both elasticsearch and opensearch backends + test-matrix Run GitHub workflow matrix testing + install Install the chart for testing + upgrade Upgrade existing installation + uninstall Uninstall the test deployment + validate Validate deployment health + load-data Load sample data into the API + cleanup Clean up all test resources + ci Run CI pipeline (lint + test + validate) + +Options: + -b, --backend BACKEND Backend to test (elasticsearch|opensearch) [default: elasticsearch] + -n, --namespace NS Kubernetes namespace [default: stac-fastapi] + -r, --release NAME Helm release name [default: stac-fastapi-test] + -m, --matrix Run matrix testing for CI + -h, --help Show this help message + +Examples: + $0 lint # Lint the chart + $0 -b opensearch install # Install with OpenSearch backend + $0 test-all # Test both backends + $0 test-matrix # Run GitHub workflow matrix tests + $0 ci # Run full CI pipeline + $0 validate # Check deployment health + $0 load-data # Load test data + $0 cleanup # Clean up everything + +EOF +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -b|--backend) + BACKEND="$2" + shift 2 + ;; + -n|--namespace) + NAMESPACE="$2" + shift 2 + ;; + -r|--release) + RELEASE_NAME="$2" + shift 2 + ;; + -m|--matrix) + MATRIX_MODE=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + lint|test|test-all|test-matrix|install|upgrade|uninstall|validate|load-data|cleanup|ci) + COMMAND="$1" + shift + ;; + *) + log_error "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +# Validate backend +if [[ "$BACKEND" != "elasticsearch" && "$BACKEND" != "opensearch" ]]; then + log_error "Invalid backend: $BACKEND. Must be 'elasticsearch' or 'opensearch'" + exit 1 +fi + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + if ! command -v helm &> /dev/null; then + log_error "Helm is not installed" + exit 1 + fi + + if ! command -v kubectl &> /dev/null; then + log_error "kubectl is not installed" + exit 1 + fi + + if ! kubectl cluster-info &> /dev/null; then + log_error "Cannot connect to Kubernetes cluster" + exit 1 + fi + + log_success "Prerequisites check passed" +} + +# Lint the Helm chart +lint_chart() { + log_info "Linting Helm chart..." + + if [[ ! -d "$CHART_PATH" ]]; then + log_error "Chart path not found: $CHART_PATH" + exit 1 + fi + + # Update dependencies + log_info "Updating chart dependencies..." + helm dependency update "$CHART_PATH" + + # Lint the chart + helm lint "$CHART_PATH" + + # Template the chart to check for syntax errors + log_info "Testing chart templates..." + helm template test-release "$CHART_PATH" \ + --set backend="$BACKEND" \ + --set "${BACKEND}.enabled=true" \ + --output-dir /tmp/helm-test || { + log_error "Chart templating failed" + exit 1 + } + + log_success "Chart linting completed successfully" +} + +# Run Helm chart tests +test_chart() { + log_info "Running Helm chart tests..." + + # Dry run installation + log_info "Testing chart installation (dry run)..." + helm install "$RELEASE_NAME" "$CHART_PATH" \ + --namespace "$NAMESPACE" \ + --create-namespace \ + --dry-run \ + --set backend="$BACKEND" \ + --set "${BACKEND}.enabled=true" \ + --set "app.image.tag=latest" + + log_success "Chart tests completed successfully" +} + +# Install the chart +install_chart() { + log_info "Installing STAC FastAPI chart with $BACKEND backend..." + + # Create namespace if it doesn't exist + kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + + # Update dependencies + helm dependency update "$CHART_PATH" + + # Select appropriate values file for backend + local values_file="" + case $BACKEND in + elasticsearch) + values_file="values-elasticsearch.yaml" + ;; + opensearch) + values_file="values-opensearch.yaml" + ;; + *) + log_error "Unknown backend: $BACKEND" + return 1 + ;; + esac + + log_info "Using values file: $values_file" + + # Install the chart with appropriate values + helm install "$RELEASE_NAME" "$CHART_PATH" \ + --namespace "$NAMESPACE" \ + --values "$CHART_PATH/$values_file" \ + --set backend="$BACKEND" \ + --set "${BACKEND}.enabled=true" \ + --set "app.image.tag=latest" \ + --set "app.service.type=ClusterIP" \ + --wait \ + --timeout=10m + + log_success "Chart installed successfully" + + # Show installation status + helm status "$RELEASE_NAME" -n "$NAMESPACE" +} + +# Upgrade the chart +upgrade_chart() { + log_info "Upgrading STAC FastAPI chart..." + + helm dependency update "$CHART_PATH" + + # Select appropriate values file for backend + local values_file="" + case $BACKEND in + elasticsearch) + values_file="values-elasticsearch.yaml" + ;; + opensearch) + values_file="values-opensearch.yaml" + ;; + *) + log_error "Unknown backend: $BACKEND" + return 1 + ;; + esac + + helm upgrade "$RELEASE_NAME" "$CHART_PATH" \ + --namespace "$NAMESPACE" \ + --values "$CHART_PATH/$values_file" \ + --set backend="$BACKEND" \ + --set "${BACKEND}.enabled=true" \ + --set "app.image.tag=latest" \ + --wait \ + --timeout=10m + + log_success "Chart upgraded successfully" +} + +# Uninstall the chart +uninstall_chart() { + log_info "Uninstalling STAC FastAPI chart..." + + helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" || true + + log_success "Chart uninstalled" +} + +# Validate deployment +validate_deployment() { + log_info "Validating deployment health..." + + # Check if deployment exists + if ! kubectl get deployment "$RELEASE_NAME" -n "$NAMESPACE" &> /dev/null; then + log_error "Deployment not found: $RELEASE_NAME" + exit 1 + fi + + # Check pod status + log_info "Checking pod status..." + kubectl get pods -n "$NAMESPACE" -l "app.kubernetes.io/name=stac-fastapi" + + # Wait for pods to be ready + log_info "Waiting for pods to be ready..." + kubectl wait --for=condition=ready pod \ + -l "app.kubernetes.io/name=stac-fastapi" \ + -n "$NAMESPACE" \ + --timeout=300s + + # Check service + log_info "Checking service..." + kubectl get service "$RELEASE_NAME" -n "$NAMESPACE" + + # Test API endpoint + log_info "Testing API endpoint..." + kubectl port-forward -n "$NAMESPACE" service/"$RELEASE_NAME" 8080:80 & + PORT_FORWARD_PID=$! + + # Wait for port-forward to be ready + sleep 5 + + # Test the API + if curl -s -f http://localhost:8080/ > /dev/null; then + log_success "API is responding" + + # Test specific endpoints + log_info "Testing API endpoints..." + + # Root endpoint + curl -s http://localhost:8080/ | jq -r '.title // "No title"' 2>/dev/null || echo "Root endpoint accessible" + + # Collections endpoint + if curl -s -f http://localhost:8080/collections > /dev/null; then + log_success "Collections endpoint accessible" + else + log_warning "Collections endpoint not accessible" + fi + + # Search endpoint + if curl -s -f -X POST http://localhost:8080/search -H "Content-Type: application/json" -d '{}' > /dev/null; then + log_success "Search endpoint accessible" + else + log_warning "Search endpoint not accessible" + fi + + else + log_error "API is not responding" + fi + + # Clean up port-forward + kill $PORT_FORWARD_PID 2>/dev/null || true + + # Check database connectivity + log_info "Checking database connectivity..." + DB_POD=$(kubectl get pods -n "$NAMESPACE" -l "app=${RELEASE_NAME}-${BACKEND}-master" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || echo "") + + if [[ -n "$DB_POD" ]]; then + if kubectl exec -n "$NAMESPACE" "$DB_POD" -- curl -s -f http://localhost:9200/_health > /dev/null 2>&1; then + log_success "Database is healthy" + else + log_warning "Database health check failed" + fi + else + log_warning "Database pod not found (might be using external database)" + fi + + log_success "Deployment validation completed" +} + +# Load sample data +load_sample_data() { + log_info "Loading sample data..." + + # Port forward to the service + kubectl port-forward -n "$NAMESPACE" service/"$RELEASE_NAME" 8080:80 & + PORT_FORWARD_PID=$! + + # Wait for port-forward + sleep 5 + + # Create a simple collection + log_info "Creating test collection..." + curl -X POST http://localhost:8080/collections \ + -H "Content-Type: application/json" \ + -d '{ + "id": "test-collection", + "title": "Test Collection", + "description": "A test collection for validation", + "extent": { + "spatial": { + "bbox": [[-180, -90, 180, 90]] + }, + "temporal": { + "interval": [["2020-01-01T00:00:00Z", "2024-12-31T23:59:59Z"]] + } + }, + "license": "public-domain" + }' || log_warning "Failed to create collection (might already exist)" + + # Create a test item + log_info "Creating test item..." + curl -X POST http://localhost:8080/collections/test-collection/items \ + -H "Content-Type: application/json" \ + -d '{ + "id": "test-item-001", + "type": "Feature", + "stac_version": "1.0.0", + "collection": "test-collection", + "geometry": { + "type": "Polygon", + "coordinates": [[[-1, -1], [1, -1], [1, 1], [-1, 1], [-1, -1]]] + }, + "bbox": [-1, -1, 1, 1], + "properties": { + "datetime": "2023-06-15T12:00:00Z" + }, + "assets": { + "thumbnail": { + "href": "https://example.com/thumbnail.jpg", + "type": "image/jpeg", + "title": "Thumbnail" + } + } + }' || log_warning "Failed to create item" + + # Test search + log_info "Testing search functionality..." + SEARCH_RESULT=$(curl -s -X POST http://localhost:8080/search \ + -H "Content-Type: application/json" \ + -d '{"collections": ["test-collection"], "limit": 1}') + + ITEM_COUNT=$(echo "$SEARCH_RESULT" | jq -r '.features | length' 2>/dev/null || echo "0") + + if [[ "$ITEM_COUNT" -gt 0 ]]; then + log_success "Sample data loaded and searchable (found $ITEM_COUNT items)" + else + log_warning "Sample data might not be immediately searchable (indexing delay)" + fi + + # Clean up port-forward + kill $PORT_FORWARD_PID 2>/dev/null || true + + log_success "Sample data loading completed" +} + +# Test both backends comprehensively +test_all_backends() { + log_info "Testing all backends (elasticsearch and opensearch)..." + + local original_backend="$BACKEND" + local failed_backends=() + + for backend in elasticsearch opensearch; do + log_info "================== Testing $backend backend ==================" + + BACKEND="$backend" + RELEASE_NAME="stac-fastapi-test-$backend" + NAMESPACE="stac-fastapi-$backend" + + log_info "Running tests for $backend backend..." + + # Run comprehensive test + if run_backend_test "$backend"; then + log_success "$backend backend tests passed" + else + log_error "$backend backend tests failed" + failed_backends+=("$backend") + fi + + # Cleanup between tests + cleanup + sleep 5 + done + + # Restore original values + BACKEND="$original_backend" + RELEASE_NAME="stac-fastapi-test" + NAMESPACE="stac-fastapi" + + # Report results + if [[ ${#failed_backends[@]} -eq 0 ]]; then + log_success "All backend tests passed successfully!" + else + log_error "The following backends failed: ${failed_backends[*]}" + exit 1 + fi +} + +# Run GitHub workflow matrix testing +test_matrix() { + log_info "Running GitHub workflow matrix testing..." + + if [[ "$MATRIX_MODE" == "true" ]]; then + log_info "Matrix mode enabled - testing single backend: $BACKEND" + run_backend_test "$BACKEND" + else + log_info "Running full matrix test locally..." + test_all_backends + fi +} + +# Run tests for a specific backend +run_backend_test() { + local backend="$1" + local test_failed=false + + log_info "Starting comprehensive test for $backend backend..." + + # Set backend-specific values + BACKEND="$backend" + + # Step 1: Lint + log_info "Step 1: Linting chart for $backend..." + if ! lint_chart; then + log_error "Linting failed for $backend" + return 1 + fi + + # Step 2: Template test + log_info "Step 2: Testing chart templates for $backend..." + if ! test_chart; then + log_error "Template test failed for $backend" + return 1 + fi + + # Step 3: Install and validate + log_info "Step 3: Installing chart for $backend..." + if ! install_chart; then + log_error "Installation failed for $backend" + return 1 + fi + + # Step 4: Validate deployment + log_info "Step 4: Validating deployment for $backend..." + if ! validate_deployment; then + log_error "Deployment validation failed for $backend" + test_failed=true + fi + + # Step 5: Load and test data + log_info "Step 5: Testing data operations for $backend..." + if ! load_sample_data; then + log_error "Data operations failed for $backend" + test_failed=true + fi + + # Step 6: Test backend-specific functionality + log_info "Step 6: Testing $backend-specific functionality..." + if ! test_backend_specifics "$backend"; then + log_error "Backend-specific tests failed for $backend" + test_failed=true + fi + + if [[ "$test_failed" == "true" ]]; then + log_error "Some tests failed for $backend backend" + return 1 + else + log_success "All tests passed for $backend backend" + return 0 + fi +} + +# Test backend-specific functionality +test_backend_specifics() { + local backend="$1" + + log_info "Testing $backend-specific functionality..." + + # Port forward to the service + kubectl port-forward -n "$NAMESPACE" service/"$RELEASE_NAME" 8080:80 & + local pf_pid=$! + + # Wait for port-forward + sleep 5 + + case "$backend" in + elasticsearch) + # Test Elasticsearch-specific endpoints + log_info "Testing Elasticsearch-specific features..." + + # Check if using correct image + local image=$(kubectl get deployment "$RELEASE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].image}') + if [[ "$image" == *"-es:"* ]]; then + log_success "Using correct Elasticsearch image: $image" + else + log_warning "Image might not be Elasticsearch-specific: $image" + fi + + # Test backend environment variable + local backend_env=$(kubectl get deployment "$RELEASE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="BACKEND")].value}') + if [[ "$backend_env" == "elasticsearch" ]]; then + log_success "Backend environment correctly set to elasticsearch" + else + log_error "Backend environment incorrect: $backend_env" + kill $pf_pid 2>/dev/null || true + return 1 + fi + ;; + + opensearch) + # Test OpenSearch-specific endpoints + log_info "Testing OpenSearch-specific features..." + + # Check if using correct image + local image=$(kubectl get deployment "$RELEASE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].image}') + if [[ "$image" == *"-os:"* ]]; then + log_success "Using correct OpenSearch image: $image" + else + log_warning "Image might not be OpenSearch-specific: $image" + fi + + # Test backend environment variable + local backend_env=$(kubectl get deployment "$RELEASE_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="BACKEND")].value}') + if [[ "$backend_env" == "opensearch" ]]; then + log_success "Backend environment correctly set to opensearch" + else + log_error "Backend environment incorrect: $backend_env" + kill $pf_pid 2>/dev/null || true + return 1 + fi + ;; + esac + + # Test that only the correct backend is deployed + log_info "Verifying only $backend backend is deployed..." + + local es_pods=$(kubectl get pods -n "$NAMESPACE" -l "app=stac-elasticsearch-master" --no-headers 2>/dev/null | wc -l) + local os_pods=$(kubectl get pods -n "$NAMESPACE" -l "app.kubernetes.io/name=opensearch" --no-headers 2>/dev/null | wc -l) + + case "$backend" in + elasticsearch) + if [[ "$es_pods" -gt 0 && "$os_pods" -eq 0 ]]; then + log_success "Only Elasticsearch backend is deployed" + else + log_error "Wrong backends deployed - ES pods: $es_pods, OS pods: $os_pods" + kill $pf_pid 2>/dev/null || true + return 1 + fi + ;; + opensearch) + if [[ "$os_pods" -gt 0 && "$es_pods" -eq 0 ]]; then + log_success "Only OpenSearch backend is deployed" + else + log_error "Wrong backends deployed - ES pods: $es_pods, OS pods: $os_pods" + kill $pf_pid 2>/dev/null || true + return 1 + fi + ;; + esac + + # Clean up port-forward + kill $pf_pid 2>/dev/null || true + + log_success "$backend-specific tests completed successfully" + return 0 +} + +# Run CI pipeline +run_ci_pipeline() { + log_info "Running CI pipeline..." + + if [[ "$MATRIX_MODE" == "true" ]]; then + log_info "CI Matrix Mode: Testing $BACKEND backend" + + # Set CI-specific configuration + export HELM_EXPERIMENTAL_OCI=1 + + # Run the backend test + if run_backend_test "$BACKEND"; then + log_success "CI pipeline passed for $BACKEND backend" + + # Generate test report + generate_test_report "$BACKEND" + else + log_error "CI pipeline failed for $BACKEND backend" + exit 1 + fi + else + log_info "CI Full Mode: Testing all backends" + test_all_backends + + # Generate combined test report + generate_test_report "all" + fi +} + +# Generate test report +generate_test_report() { + local backend="$1" + local report_file="test-report-$backend-$(date +%Y%m%d-%H%M%S).json" + + log_info "Generating test report: $report_file" + + # Get cluster info + local k8s_version=$(kubectl version --short 2>/dev/null | grep "Server Version" | cut -d: -f2 | xargs) + local helm_version=$(helm version --short 2>/dev/null) + + # Create test report + cat > "$report_file" << EOF +{ + "test_run": { + "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "backend": "$backend", + "kubernetes_version": "$k8s_version", + "helm_version": "$helm_version", + "chart_version": "$(helm show chart $CHART_PATH | grep version | head -1 | cut -d: -f2 | xargs)", + "status": "passed" + }, + "tests_performed": [ + "chart_linting", + "template_validation", + "deployment_installation", + "health_validation", + "data_operations", + "backend_specific_tests" + ], + "resources_tested": { + "deployment": true, + "service": true, + "configmap": true, + "secrets": true, + "backend_statefulset": true + } +} +EOF + + log_success "Test report generated: $report_file" +} + +# Clean up all resources +cleanup() { + log_info "Cleaning up all test resources..." + + # Uninstall Helm release + helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" --ignore-not-found + + # Delete namespace (this will delete all resources) + kubectl delete namespace "$NAMESPACE" --ignore-not-found + + # Clean up any remaining persistent volumes + kubectl get pv | grep "$NAMESPACE" | awk '{print $1}' | xargs -r kubectl delete pv + + log_success "Cleanup completed" +} + +# Main script logic +main() { + case "${COMMAND:-}" in + lint) + check_prerequisites + lint_chart + ;; + test) + check_prerequisites + lint_chart + test_chart + ;; + test-all) + check_prerequisites + test_all_backends + ;; + test-matrix) + check_prerequisites + test_matrix + ;; + ci) + check_prerequisites + run_ci_pipeline + ;; + install) + check_prerequisites + install_chart + ;; + upgrade) + check_prerequisites + upgrade_chart + ;; + uninstall) + uninstall_chart + ;; + validate) + check_prerequisites + validate_deployment + ;; + load-data) + check_prerequisites + load_sample_data + ;; + cleanup) + cleanup + ;; + "") + log_error "No command specified" + show_help + exit 1 + ;; + *) + log_error "Unknown command: $COMMAND" + show_help + exit 1 + ;; + esac +} + +# Trap to ensure cleanup on script exit +trap 'kill $PORT_FORWARD_PID 2>/dev/null || true' EXIT + +# Run main function +main \ No newline at end of file