diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..e6b34f2a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,68 @@
+# --- VERSION CONTROL ---
+.git
+.gitignore
+.gitattributes
+.hg
+.svn
+
+# --- DOCKER & KUBERNETES ---
+Dockerfile
+docker-compose.yml
+.dockerignore
+.helmignore
+charts/
+values.yaml
+
+# --- OS / IDE SPECIFIC ---
+.DS_Store
+Thumbs.db
+.vscode/
+.idea/
+*.swp
+*.swo
+.project
+.classpath
+.settings/
+
+# --- LOCAL DEVELOPMENT / SECRETS ---
+.env
+.env.*
+!.env.example
+*.pem
+*.key
+*.p12
+*.cert
+ssh-key*
+kubeconfig*
+secrets.txt
+
+# --- PACKAGE MANAGER CACHES ---
+**/node_modules/
+**/.npm/
+**/.yarn/
+.gradle/
+.mvn/
+target/
+bin/
+obj/
+__pycache__/
+*.pyc
+.venv/
+vendor/
+
+# --- BUILD OUTPUTS & LOGS ---
+dist/
+build/
+out/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.terraform/
+.serverless/
+.tar
+
+# --- DOCUMENTATION ---
+README.md
+CHANGELOG.md
+docs/
diff --git a/.github/workflows/operator-build-publish.yml b/.github/workflows/operator-build-publish.yml
new file mode 100644
index 00000000..7d9270dc
--- /dev/null
+++ b/.github/workflows/operator-build-publish.yml
@@ -0,0 +1,269 @@
+name: Build and Publish Operator
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'operators/**'
+ - '.github/workflows/operator-build-publish.yml'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'operators/**'
+ types: [opened, synchronize, reopened]
+
+env:
+ REGISTRY: quay.io
+ IMAGE_NAME: geospatial-studio/geostudio-operator
+ CHART_VERSION: 0.1.4
+
+jobs:
+ detect-changes:
+ runs-on: ubuntu-latest
+ outputs:
+ operator_changed: ${{ steps.filter.outputs.operator }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for operator changes
+ id: filter
+ run: |
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
+ git fetch origin ${{ github.base_ref }}
+ CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
+ else
+ CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
+ fi
+
+ echo "Changed files:"
+ echo "$CHANGED_FILES"
+
+ if echo "$CHANGED_FILES" | grep -q "^operators/"; then
+ echo "operator=true" >> $GITHUB_OUTPUT
+ echo "Operator files changed - build will proceed"
+ else
+ echo "operator=false" >> $GITHUB_OUTPUT
+ echo "No operator files changed - skipping build"
+ fi
+
+ build-operator:
+ needs: detect-changes
+ if: needs.detect-changes.outputs.operator_changed == 'true'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ outputs:
+ image_tag: ${{ steps.meta.outputs.tags }}
+ image_digest: ${{ steps.build.outputs.digest }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Quay.io
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ secrets.QUAY_USERNAME }}
+ password: ${{ secrets.QUAY_PASSWORD }}
+
+ - name: Extract metadata for operator image
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=sha,prefix=sha-
+ type=raw,value=latest,enable={{is_default_branch}}
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+
+ - name: Build operator image
+ id: build
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile.operator
+ build-args: |
+ CHART_VERSION=${{ env.CHART_VERSION }}
+ push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ platforms: linux/amd64,linux/arm64
+
+ - name: Generate build summary
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ run: |
+ echo "## Operator Build Summary" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Image Details" >> $GITHUB_STEP_SUMMARY
+ echo "- **Registry:** ${{ env.REGISTRY }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Image:** ${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Tags:**" >> $GITHUB_STEP_SUMMARY
+ echo "${{ steps.meta.outputs.tags }}" | sed 's/^/ - /' >> $GITHUB_STEP_SUMMARY
+ echo "- **Digest:** ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Chart Version:** ${{ env.CHART_VERSION }}" >> $GITHUB_STEP_SUMMARY
+
+ test-operator:
+ needs: [detect-changes, build-operator]
+ if: needs.detect-changes.outputs.operator_changed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up kind cluster
+ uses: helm/kind-action@v1
+ with:
+ cluster_name: operator-test
+ wait: 30s
+
+ - name: Install operator-sdk
+ run: |
+ export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
+ export OS=$(uname | awk '{print tolower($0)}')
+ export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.42.0
+ curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
+ chmod +x operator-sdk_${OS}_${ARCH}
+ sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk
+
+ - name: Verify operator manifests
+ working-directory: ./operators
+ run: |
+ # Verify watches.yaml exists and is valid
+ if [ ! -f watches.yaml ]; then
+ echo "Error: watches.yaml not found"
+ exit 1
+ fi
+
+ # Verify PROJECT file
+ if [ ! -f PROJECT ]; then
+ echo "Error: PROJECT file not found"
+ exit 1
+ fi
+
+ # Check CRD manifests
+ if [ ! -d config/crd ]; then
+ echo "Error: CRD directory not found"
+ exit 1
+ fi
+
+ echo "✓ Operator manifests verified"
+
+ - name: Run operator validation
+ working-directory: ./operators
+ run: |
+ # Validate bundle if it exists
+ if [ -d bundle ]; then
+ operator-sdk bundle validate ./bundle || echo "Bundle validation skipped"
+ fi
+
+ echo "✓ Operator validation completed"
+
+ create-release:
+ needs: [detect-changes, build-operator, test-operator]
+ if: |
+ github.event_name == 'push' &&
+ github.ref == 'refs/heads/main' &&
+ needs.detect-changes.outputs.operator_changed == 'true'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get operator version
+ id: version
+ working-directory: ./operators
+ run: |
+ VERSION=$(grep '^VERSION ?=' Makefile | cut -d'=' -f2 | tr -d ' ')
+ CHART_VERSION=$(grep '^CHART_VERSION ?=' Makefile | cut -d'=' -f2 | tr -d ' ')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "chart_version=$CHART_VERSION" >> $GITHUB_OUTPUT
+ echo "Operator Version: $VERSION"
+ echo "Chart Version: $CHART_VERSION"
+
+ - name: Check if tag exists
+ id: check_tag
+ run: |
+ TAG="operator-v${{ steps.version.outputs.version }}"
+ if git rev-parse "$TAG" >/dev/null 2>&1; then
+ echo "exists=true" >> $GITHUB_OUTPUT
+ echo "Tag $TAG already exists"
+ else
+ echo "exists=false" >> $GITHUB_OUTPUT
+ echo "Tag $TAG does not exist"
+ fi
+
+ - name: Create release notes
+ if: steps.check_tag.outputs.exists == 'false'
+ id: release_notes
+ run: |
+ cat > release_notes.md << EOF
+ ## GEOStudio Operator Release v${{ steps.version.outputs.version }}
+
+ ### Changes
+ $(git log --pretty=format:"- %s (%h)" --grep="operator" -i HEAD~5..HEAD || echo "- Operator updates and improvements")
+
+ ### Image Details
+ - **Registry:** ${{ env.REGISTRY }}
+ - **Image:** ${{ env.IMAGE_NAME }}:v${{ steps.version.outputs.version }}
+ - **Chart Version:** ${{ steps.version.outputs.chart_version }}
+ - **Digest:** ${{ needs.build-operator.outputs.image_digest }}
+
+ ### Installation
+ \`\`\`bash
+ # Using the operator image
+ kubectl apply -f https://github.com/${{ github.repository }}/releases/download/operator-v${{ steps.version.outputs.version }}/operator-install.yaml
+
+ # Or using make
+ cd operators
+ make install IMG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${{ steps.version.outputs.version }}
+ \`\`\`
+
+ ### Documentation
+ See the [operator documentation](https://github.com/${{ github.repository }}/tree/main/operators) for more details.
+ EOF
+
+ cat release_notes.md
+
+ - name: Create GitHub Release
+ if: steps.check_tag.outputs.exists == 'false'
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: operator-v${{ steps.version.outputs.version }}
+ name: Operator v${{ steps.version.outputs.version }}
+ body_path: release_notes.md
+ draft: false
+ prerelease: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ notify:
+ needs: [detect-changes, build-operator, test-operator, create-release]
+ if: always() && needs.detect-changes.outputs.operator_changed == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Build status summary
+ run: |
+ echo "## Workflow Summary" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Build:** ${{ needs.build-operator.result }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Tests:** ${{ needs.test-operator.result }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Release:** ${{ needs.create-release.result }}" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitignore b/.gitignore
index f894c4c5..6a8935e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,20 @@ workspace/
minio-private.key
minio-public.crt
+# mkdocs documentation
+**/site/
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+*.tar
+bin
+
+# editor and IDE paraphernalia
+.idea
+*.swp
+*.swo
+*~
diff --git a/Dockerfile.operator b/Dockerfile.operator
new file mode 100644
index 00000000..f649692f
--- /dev/null
+++ b/Dockerfile.operator
@@ -0,0 +1,21 @@
+# Stage 1: Pull the Helm chart from OCI registry
+FROM alpine/helm:latest AS chart-puller
+
+# Set up Helm and pull the chart
+WORKDIR /charts
+ARG CHART_VERSION=0.1.4
+ARG CHART_REGISTRY=oci://quay.io/geospatial-studio/charts
+ARG CHART_NAME=geospatial-studio
+
+# Install necessary tools
+RUN apk add --no-cache docker-cli
+
+RUN helm pull ${CHART_REGISTRY}/${CHART_NAME} --version ${CHART_VERSION} --untar
+
+# Stage 2: Build the operator image
+FROM quay.io/operator-framework/helm-operator:v1.42.0
+
+ENV HOME=/opt/helm
+COPY operators/watches.yaml ${HOME}/watches.yaml
+COPY --from=chart-puller /charts/geospatial-studio ${HOME}/helm-charts/geospatial-studio
+WORKDIR ${HOME}
diff --git a/Dockerfile.operator.local b/Dockerfile.operator.local
new file mode 100644
index 00000000..1da050ee
--- /dev/null
+++ b/Dockerfile.operator.local
@@ -0,0 +1,6 @@
+FROM quay.io/operator-framework/helm-operator:v1.42.0
+
+ENV HOME=/opt/helm
+COPY operators/watches.yaml ${HOME}/watches.yaml
+COPY geospatial-studio ${HOME}/helm-charts/geospatial-studio
+WORKDIR ${HOME}
diff --git a/deploy_studio_k8s.sh b/deploy_studio_k8s.sh
index 6abd064d..eaaf480f 100755
--- a/deploy_studio_k8s.sh
+++ b/deploy_studio_k8s.sh
@@ -103,8 +103,10 @@ get_menu_selection \
storage_mode \
"$storage_mode_options"
+source lib/k8s-utils.sh
+CSI_DRIVER_TYPE=$(get_csi_driver_type)
export STORAGE_MODE=$storage_mode
-echo "STORAGE_MODE selected: **$STORAGE_MODE**"
+echo "Using CSI driver type: $CSI_DRIVER_TYPE with storage class: $STORAGE_MODE"
# Update env.sh with storage mode
sed -i -e "s/export STORAGE_MODE=.*/export STORAGE_MODE=${STORAGE_MODE}/g" workspace/${DEPLOYMENT_ENV}/env/env.sh
@@ -201,7 +203,7 @@ if [[ "$DEPLOY_MINIO" == "Deploy" ]]; then
kubectl port-forward -n ${OC_PROJECT} svc/minio 9001:9001 >> studio-pf.log 2>&1 &
sleep 5
- cp -R deployment-scripts/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
+ cp -R geospatial-studio/files/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-s3fs-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-sc.yaml
kubectl apply -k workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/
diff --git a/deploy_studio_lima.sh b/deploy_studio_lima.sh
index a47f67e4..1746c7c1 100755
--- a/deploy_studio_lima.sh
+++ b/deploy_studio_lima.sh
@@ -105,7 +105,7 @@ if [[ "$DEPLOY_MINIO" == "Deploy" ]]; then
kubectl port-forward -n ${OC_PROJECT} svc/minio 9001:9001 >> studio-pf.log 2>&1 &
sleep 5
- cp -R deployment-scripts/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
+ cp -R geospatial-studio/files/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-s3fs-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-sc.yaml
kubectl apply -k workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/
@@ -125,8 +125,10 @@ if [[ "$DEPLOY_MINIO" == "Deploy" ]]; then
echo "Lima deployment uses cloud-object-storage mode with MinIO (default)"
echo "***********************************************************************************"
- export STORAGE_MODE="cloud-object-storage"
- echo "STORAGE_MODE set to: **$STORAGE_MODE**"
+ export STORAGE_MODE="cloud-object-storage"
+ source lib/k8s-utils.sh
+ CSI_DRIVER_TYPE=$(get_csi_driver_type)
+ echo "Using CSI driver type: $CSI_DRIVER_TYPE with storage class: $STORAGE_MODE"
# Update env.sh with storage mode
sed -i -e "s/export STORAGE_MODE=.*/export STORAGE_MODE=${STORAGE_MODE}/g" workspace/${DEPLOYMENT_ENV}/env/env.sh
diff --git a/deployment-scripts/minio-openssl.conf b/deployment-scripts/minio-openssl.conf
index 86b8ae47..a326ebed 100644
--- a/deployment-scripts/minio-openssl.conf
+++ b/deployment-scripts/minio-openssl.conf
@@ -12,6 +12,7 @@ OU = ACMEU
CN = default.svc.cluster.local
[v3_req]
+basicConstraints = CA:TRUE
subjectAltName = @alt_names
[alt_names]
diff --git a/deployment-scripts/template/.env.template b/deployment-scripts/template/.env.template
index bc266fc7..b1e62193 100644
--- a/deployment-scripts/template/.env.template
+++ b/deployment-scripts/template/.env.template
@@ -1,3 +1,19 @@
+# ==============================================================================
+# GeoStudio Environment Variables Template
+# ==============================================================================
+# SECURITY WARNING:
+# This file contains DEFAULT VALUES suitable for LOCAL DEVELOPMENT ONLY.
+#
+# For PRODUCTION deployments:
+# - Generate strong, random passwords for ALL credentials
+# - Never use default passwords (devPassword, admin, etc.)
+# - Store secrets securely (e.g., using secret managers)
+# - Rotate credentials regularly
+#
+# To generate secure passwords:
+# openssl rand -base64 32 | tr '+/' '-_' | tr -d '\n'
+# ==============================================================================
+
# Initialisation
deployment_name=${DEPLOYMENT_ENV}
ocp_project=${OC_PROJECT}
@@ -72,4 +88,10 @@ tls_crt_b64=
tls_key_b64=
# Redis password
-redis_password=
+# WARNING: Change this for production deployments!
+redis_password=devPassword
+
+# Keycloak admin credentials
+# WARNING: Change these for production deployments!
+keycloak_admin_user=admin
+keycloak_admin_password=admin
diff --git a/deployment-scripts/template/env.template.sh b/deployment-scripts/template/env.template.sh
index e68e519c..533eb8f2 100644
--- a/deployment-scripts/template/env.template.sh
+++ b/deployment-scripts/template/env.template.sh
@@ -9,7 +9,7 @@
export DEPLOYMENT_ENV=${DEPLOYMENT_ENV}
export OC_PROJECT=${OC_PROJECT}
export ENVIRONMENT=
-export ROUTE_ENABLED=true
+export ROUTE_ENABLED=${ROUTE_ENABLED:-true}
export CONTAINER_IMAGE_REPOSITORY=geospatial-studio
# POSTGRESQL version for cluster DB
@@ -40,6 +40,12 @@ export REDIS_ENABLED=true
export REDIS_FULL_NAME_OVERRIDE=geofm-redis
export REDIS_ARCHITECTURE=replication
+# MinIO configuration
+export MINIO_IMAGE=quay.io/minio/minio
+export MINIO_TAG=latest
+export MINIO_PERSISTENCE_ENABLED=true
+export MINIO_STORAGE_SIZE=100Gi
+
# Pipelines configuration
export PIPELINES_ENABLED=true
@@ -79,4 +85,10 @@ export NODE_GPU_SPEC=NVIDIA-A100-SXM4-80GB
# Geoserver config
export GEOSERVER_CM_PROXYBASEURL=
-export GEOSERVER_CM_WHITELIST=
\ No newline at end of file
+export GEOSERVER_CM_WHITELIST=
+
+# Operator configuration (for operator-based deployments)
+export GEOSTUDIO_OPERATOR_IMAGE=${GEOSTUDIO_OPERATOR_IMAGE:-quay.io/geospatial-studio/geostudio-operator:latest}
+export INSTALL_IBM_CSI_DRIVER=${INSTALL_IBM_CSI_DRIVER:-true}
+export USER_NAMESPACE=${OC_PROJECT}
+export OPERATOR_NAMESPACE=${OPERATOR_NAMESPACE:-geostudio-operator-system}
diff --git a/deployment-scripts/values-file-generate.sh b/deployment-scripts/values-file-generate.sh
index 9c46c328..fa8c2bec 100755
--- a/deployment-scripts/values-file-generate.sh
+++ b/deployment-scripts/values-file-generate.sh
@@ -4,9 +4,8 @@
# SPDX-License-Identifier: Apache-2.0
-
-
source workspace/$DEPLOYMENT_ENV/env/env.sh
+source workspace/$DEPLOYMENT_ENV/env/.env
charts=(
geospatial-studio
@@ -63,5 +62,8 @@ do
-e "s|GEOSERVER_CM_PROXYBASEURL|${GEOSERVER_CM_PROXYBASEURL}|" \
-e "s|geospatial-studio|${CONTAINER_IMAGE_REPOSITORY}|" \
-e "s|PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE|${PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE}|" \
+ -e "s|PIPELINES_TERRATORCH_INFERENCE_CREATE_FT_PVC|${PIPELINES_TERRATORCH_INFERENCE_CREATE_FT_PVC}|" \
+ -e "s|cesium_token|${cesium_token}|" \
+ -e "s|mapbox_token|${mapbox_token}|" \
${HELM_CHART_NAME}/values.yaml > workspace/$DEPLOYMENT_ENV/values/${HELM_CHART_NAME}/values.yaml
done
diff --git a/docs/deployment/detailed_deployment_k8s.md b/docs/deployment/detailed_deployment_k8s.md
index 58657d89..9a6e3163 100644
--- a/docs/deployment/detailed_deployment_k8s.md
+++ b/docs/deployment/detailed_deployment_k8s.md
@@ -150,7 +150,7 @@ The IBM Object CSI Driver enables dynamic provisioning of S3-compatible storage:
```bash
# Apply the CSI driver manifests
-cp -R deployment-scripts/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
+cp -R geospatial-studio/files/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-s3fs-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-sc.yaml
kubectl apply -k workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/
@@ -803,7 +803,7 @@ kubectl delete -f deployment-scripts/minio-deployment.yaml -n ${OC_PROJECT}
kubectl delete -f deployment-scripts/geoserver-deployment.yaml -n ${OC_PROJECT}
# Delete CSI driver
-kubectl delete -k deployment-scripts/ibm-object-csi-driver/
+kubectl delete -k geospatial-studio/files/ibm-object-csi-driver/
# Delete secrets and configmaps
kubectl delete secret minio-tls-secret -n ${OC_PROJECT}
diff --git a/docs/deployment/detailed_deployment_local.md b/docs/deployment/detailed_deployment_local.md
index a7fed723..ffd6d586 100644
--- a/docs/deployment/detailed_deployment_local.md
+++ b/docs/deployment/detailed_deployment_local.md
@@ -493,13 +493,16 @@ kubectl port-forward -n ${OC_PROJECT} svc/minio 9000:9000 &
# Login with username: `minioadmin`, password: `minioadmin`
-
# Also at this point update `workspace/${DEPLOYMENT_ENV}/env/.env.sh` with...
export COS_STORAGE_CLASS=cos-s3-csi-s3fs-sc
export NON_COS_STORAGE_CLASS=local-path
-```
-
+# Install the drivers
+cp -R geospatial-studio/files/ibm-object-csi-driver workspace/$DEPLOYMENT_ENV/initialisation
+sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-s3fs-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
+sed -e "s/default/$OC_PROJECT/g" deployment-scripts/template/cos-s3-csi-sc.yaml > workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/cos-s3-csi-sc.yaml
+kubectl apply -k workspace/$DEPLOYMENT_ENV/initialisation/ibm-object-csi-driver/
+```
* Once the S3 instance has been created, you can add the credentials and endpoint to the `workspace/${DEPLOYMENT_ENV}/env/.env` file as shown below.
diff --git a/geospatial-studio/charts/gfm-studio-gateway/templates/job.yaml b/geospatial-studio/charts/gfm-studio-gateway/templates/job.yaml
index 33e3059e..d355adbb 100644
--- a/geospatial-studio/charts/gfm-studio-gateway/templates/job.yaml
+++ b/geospatial-studio/charts/gfm-studio-gateway/templates/job.yaml
@@ -78,6 +78,23 @@ spec:
echo "Gateway service is ready!"
{{- end }}
{{- end }}
+ # Init container to wait for gateway service to be ready
+ {{- if hasKey $config "waitForGateway" }}
+ {{- if $config.waitForGateway.enabled }}
+ - name: wait-for-gateway
+ image: {{ $config.waitForGateway.image | default "busybox:1.36" }}
+ imagePullPolicy: {{ $values.image.pullPolicy | default $values.global.imagePullPolicy }}
+ command: ['sh', '-c']
+ args:
+ - |
+ echo "Waiting for gateway service to be ready..."
+ until nc -z {{ $values.global.appNames.ui }}.{{ $values.global.namespace }}.svc.cluster.local {{ $config.waitForGateway.port | default $values.global.oauth.oauthProxyPort }}; do
+ echo "Gateway service not ready yet, retrying in {{ $config.waitForGateway.sleepTime | default 5 }} seconds..."
+ sleep {{ $config.waitForGateway.sleepTime | default 5 }}
+ done
+ echo "Gateway service is ready!"
+ {{- end }}
+ {{- end }}
containers:
- name: {{ $hook_name }}-job
{{- if hasKey $config "image" }}
diff --git a/geospatial-studio/charts/gfm-studio-gateway/values.yaml b/geospatial-studio/charts/gfm-studio-gateway/values.yaml
index 5ee5073a..e9c7da67 100644
--- a/geospatial-studio/charts/gfm-studio-gateway/values.yaml
+++ b/geospatial-studio/charts/gfm-studio-gateway/values.yaml
@@ -132,7 +132,7 @@ hooks:
MODELS_URI="$BASE_URL"/studio-gateway/v2/models
MAX_RETRIES=30
RETRY_DELAY=5
-
+
echo "Seeding sandbox models -> $MODELS_URI"
# Function to post model with retry until 2xx response
@@ -179,7 +179,7 @@ hooks:
# Post both models with retry logic
post_model "/payloads/try-in-lab.json" "geofm-sandbox-models"
post_model "/payloads/add-layer.json" "add-layer-sandbox-model"
-
+
echo ""
echo "✅ Sandbox model seed complete"
diff --git a/deployment-scripts/ibm-object-csi-driver/cos-s3-csi-controller.yaml b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-controller.yaml
similarity index 52%
rename from deployment-scripts/ibm-object-csi-driver/cos-s3-csi-controller.yaml
rename to geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-controller.yaml
index a5695e84..1e75310c 100644
--- a/deployment-scripts/ibm-object-csi-driver/cos-s3-csi-controller.yaml
+++ b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-controller.yaml
@@ -66,6 +66,65 @@ spec:
app: cos-s3-csi-controller
spec:
serviceAccountName: cos-s3-csi-controller
+ initContainers:
+ - name: setup-ca-cert
+ image: cos-driver-image
+ command: ["/bin/sh", "-c"]
+ args:
+ - |
+ set -e
+ mkdir -p /ca-certs
+ mkdir -p /etc-ssl-certs
+
+ # Copy MinIO CA certificate
+ if [ -f /tmp/minio-ca/minio-public.crt ]; then
+ cp /tmp/minio-ca/minio-public.crt /ca-certs/minio-ca.crt
+ echo "✓ Copied MinIO CA certificate"
+ else
+ echo "Warning: MinIO CA certificate not found at /tmp/minio-ca/minio-public.crt"
+ exit 1
+ fi
+
+ # Try to find and copy system CA certificates
+ if [ -f /etc/ssl/certs/ca-certificates.crt ]; then
+ cat /etc/ssl/certs/ca-certificates.crt > /ca-certs/ca-bundle.crt
+ echo "✓ Copied system CA certificates from /etc/ssl/certs/ca-certificates.crt"
+ elif [ -f /etc/ssl/certs/ca-bundle.crt ]; then
+ cat /etc/ssl/certs/ca-bundle.crt > /ca-certs/ca-bundle.crt
+ echo "✓ Copied system CA certificates from /etc/ssl/certs/ca-bundle.crt"
+ elif [ -f /etc/pki/tls/certs/ca-bundle.crt ]; then
+ cat /etc/pki/tls/certs/ca-bundle.crt > /ca-certs/ca-bundle.crt
+ echo "✓ Copied system CA certificates from /etc/pki/tls/certs/ca-bundle.crt"
+ else
+ echo "Warning: System CA certificates not found, using MinIO CA only"
+ touch /ca-certs/ca-bundle.crt
+ fi
+
+ # Append MinIO CA to bundle
+ cat /ca-certs/minio-ca.crt >> /ca-certs/ca-bundle.crt
+ echo "✓ Added MinIO CA certificate to bundle"
+
+ # Copy the bundle to /etc/ssl/certs for Go programs
+ cp /ca-certs/ca-bundle.crt /etc-ssl-certs/ca-certificates.crt
+ cp /ca-certs/ca-bundle.crt /etc-ssl-certs/ca-bundle.crt
+ echo "✓ Copied CA bundle to /etc/ssl/certs/"
+
+ # Verify the bundle exists and has content
+ if [ ! -s /ca-certs/ca-bundle.crt ]; then
+ echo "ERROR: CA bundle is empty"
+ exit 1
+ fi
+
+ echo "✓ CA certificate setup complete"
+ ls -lh /ca-certs/
+ ls -lh /etc-ssl-certs/
+ volumeMounts:
+ - name: trusted-ca-certs
+ mountPath: /tmp/minio-ca
+ - name: ca-certificates
+ mountPath: /ca-certs
+ - name: etc-ssl-certs
+ mountPath: /etc-ssl-certs
containers:
- name: csi-provisioner
image: csi-provisioner-image
@@ -81,6 +140,9 @@ spec:
volumeMounts:
- mountPath: /csi
name: socket-dir
+ - name: ca-certificates
+ mountPath: /ca-certs
+ readOnly: true
- name: cos-csi-provisioner
securityContext:
capabilities:
@@ -104,13 +166,18 @@ spec:
valueFrom:
fieldRef:
fieldPath: spec.nodeName
+ - name: SSL_CERT_FILE
+ value: /ca-certs/ca-bundle.crt
imagePullPolicy: "Always"
volumeMounts:
- mountPath: /csi
name: socket-dir
- - name: trusted-ca-certs
- mountPath: /etc/ssl/certs/minio-ca-certificates.crt
- subPath: minio-public.crt
+ - name: ca-certificates
+ mountPath: /ca-certs
+ readOnly: true
+ - name: etc-ssl-certs
+ mountPath: /etc/ssl/certs
+ readOnly: true
- name: liveness-probe
image: liveness-probe-image
args:
@@ -129,3 +196,7 @@ spec:
- name: trusted-ca-certs
configMap:
name: minio-public-config
+ - name: ca-certificates
+ emptyDir: {}
+ - name: etc-ssl-certs
+ emptyDir: {}
diff --git a/deployment-scripts/ibm-object-csi-driver/cos-s3-csi-driver.yaml b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-driver.yaml
similarity index 100%
rename from deployment-scripts/ibm-object-csi-driver/cos-s3-csi-driver.yaml
rename to geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-driver.yaml
diff --git a/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
new file mode 100644
index 00000000..3ad15735
--- /dev/null
+++ b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+ name: cos-s3-csi-s3fs-sc
+provisioner: cos.s3.csi.ibm.io
+mountOptions:
+ - "multipart_size=62"
+ - "max_dirty_data=51200"
+ - "parallel_count=8"
+ - "max_stat_cache_size=100000"
+ - "retries=5"
+ - "kernel_cache"
+ - "allow_other"
+parameters:
+ mounter: "s3fs"
+ client: "awss3"
+ cosEndpoint: "MINIO_ENDPOINT_PLACEHOLDER"
+ locationConstraint: "REGION_PLACEHOLDER"
+ csi.storage.k8s.io/provisioner-secret-name: ${pvc.annotations['cos.csi.driver/secret']}
+ csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}
+ csi.storage.k8s.io/node-publish-secret-name: ${pvc.annotations['cos.csi.driver/secret']}
+ csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace}
+reclaimPolicy: Retain
diff --git a/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-sc.yaml b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-sc.yaml
new file mode 100644
index 00000000..e0e0a5d4
--- /dev/null
+++ b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csi-sc.yaml
@@ -0,0 +1,15 @@
+---
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+ name: cos-s3-csi-sc
+provisioner: cos.s3.csi.ibm.io
+parameters:
+ mounter: "s3fs"
+ client: "awss3"
+ cosEndpoint: "MINIO_ENDPOINT_PLACEHOLDER"
+ locationConstraint: "REGION_PLACEHOLDER"
+ csi.storage.k8s.io/provisioner-secret-name: ${pvc.annotations['cos.csi.driver/secret']}
+ csi.storage.k8s.io/provisioner-secret-namespace: ${pvc.namespace}
+ csi.storage.k8s.io/node-publish-secret-name: ${pvc.annotations['cos.csi.driver/secret']}
+ csi.storage.k8s.io/node-publish-secret-namespace: ${pvc.namespace}
diff --git a/deployment-scripts/ibm-object-csi-driver/cos-s3-csidriver.yaml b/geospatial-studio/files/ibm-object-csi-driver/cos-s3-csidriver.yaml
similarity index 100%
rename from deployment-scripts/ibm-object-csi-driver/cos-s3-csidriver.yaml
rename to geospatial-studio/files/ibm-object-csi-driver/cos-s3-csidriver.yaml
diff --git a/deployment-scripts/ibm-object-csi-driver/kustomization.yaml b/geospatial-studio/files/ibm-object-csi-driver/kustomization.yaml
similarity index 100%
rename from deployment-scripts/ibm-object-csi-driver/kustomization.yaml
rename to geospatial-studio/files/ibm-object-csi-driver/kustomization.yaml
diff --git a/geospatial-studio/templates/_enable-postgresql.tpl b/geospatial-studio/templates/_enable-postgresql.tpl
new file mode 100644
index 00000000..c2ebcbc2
--- /dev/null
+++ b/geospatial-studio/templates/_enable-postgresql.tpl
@@ -0,0 +1,4 @@
+{{- if .Values.global.postgresql.enabled }}
+# This file ensures the postgresql subchart is enabled when global.postgresql.enabled=true
+# The actual PostgreSQL configuration is in the global.postgresql section of values.yaml
+{{- end }}
diff --git a/geospatial-studio/templates/csi-driver-configmap.yaml b/geospatial-studio/templates/csi-driver-configmap.yaml
new file mode 100644
index 00000000..8f9958e1
--- /dev/null
+++ b/geospatial-studio/templates/csi-driver-configmap.yaml
@@ -0,0 +1,40 @@
+{{- if .Values.global.csiDriver.enabled }}
+{{- if and .Values.global.minio.enabled (eq .Values.global.objectStorage.cos_storage_class "cos-s3-csi-s3fs-sc") }}
+---
+# ConfigMap containing CSI driver manifests
+# These manifests are loaded from files/ibm-object-csi-driver/ directory
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Release.Name }}-csi-driver-manifests
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-installer
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-80"
+ "helm.sh/hook-delete-policy": before-hook-creation
+data:
+ kustomization.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/kustomization.yaml" | indent 4 }}
+
+ cos-s3-csi-controller.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/cos-s3-csi-controller.yaml" | indent 4 }}
+
+ cos-s3-csi-driver.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/cos-s3-csi-driver.yaml" | indent 4 }}
+
+ cos-s3-csidriver.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/cos-s3-csidriver.yaml" | indent 4 }}
+
+ cos-s3-csi-sc.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/cos-s3-csi-sc.yaml" | indent 4 }}
+
+ cos-s3-csi-s3fs-sc.yaml: |
+{{ .Files.Get "files/ibm-object-csi-driver/cos-s3-csi-s3fs-sc.yaml" | indent 4 }}
+
+{{- end }}
+{{- end }}
diff --git a/geospatial-studio/templates/csi-driver-install-job.yaml b/geospatial-studio/templates/csi-driver-install-job.yaml
new file mode 100644
index 00000000..fc261e90
--- /dev/null
+++ b/geospatial-studio/templates/csi-driver-install-job.yaml
@@ -0,0 +1,236 @@
+{{- if .Values.global.csiDriver.enabled }}
+{{- if and .Values.global.minio.enabled (eq .Values.global.objectStorage.cos_storage_class "cos-s3-csi-s3fs-sc") }}
+
+---
+# Job to install IBM Object S3 CSI Driver
+# This job deploys the CSI driver components to enable S3 bucket mounting as PVCs
+# Runs after ConfigMap creation (-80) and MinIO TLS certificate creation (-85)
+# CSI manifests are loaded from ConfigMap created by csi-driver-configmap.yaml
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-csi-driver-installer
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-installer
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-75"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 5
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-installer
+ spec:
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ restartPolicy: Never
+ containers:
+ - name: csi-installer
+ image: bitnami/kubectl:latest
+ imagePullPolicy: IfNotPresent
+ command: ["/bin/bash"]
+ args:
+ - "-c"
+ - |
+ set -e
+
+ NAMESPACE="{{ .Values.global.namespace }}"
+ MINIO_ENDPOINT="{{ .Values.global.objectStorage.endpoint }}"
+ REGION="{{ .Values.global.objectStorage.region | default "us-east-1" }}"
+ ZONE="${REGION}a"
+ CONFIGMAP_NAME="{{ .Release.Name }}-csi-driver-manifests"
+
+ echo "=================================================="
+ echo "IBM Object S3 CSI Driver Installation"
+ echo "=================================================="
+ echo "Namespace: $NAMESPACE"
+ echo "MinIO Endpoint: $MINIO_ENDPOINT"
+ echo "Region: $REGION"
+ echo "Zone: $ZONE"
+ echo "ConfigMap: $CONFIGMAP_NAME"
+ echo "=================================================="
+ echo ""
+
+ # Add topology labels to nodes (required by CSI driver)
+ echo "Ensuring topology labels on nodes..."
+ NODES=$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}')
+ for NODE in $NODES; do
+ CURRENT_REGION=$(kubectl get node "$NODE" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/region}' 2>/dev/null || echo "")
+ CURRENT_ZONE=$(kubectl get node "$NODE" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}' 2>/dev/null || echo "")
+
+ if [ "$CURRENT_REGION" != "$REGION" ] || [ "$CURRENT_ZONE" != "$ZONE" ]; then
+ echo " Adding topology labels to node $NODE (region=$REGION, zone=$ZONE)"
+ kubectl label nodes "$NODE" \
+ topology.kubernetes.io/region="$REGION" \
+ topology.kubernetes.io/zone="$ZONE" \
+ --overwrite
+ else
+ echo " ✓ Node $NODE already has topology labels"
+ fi
+ done
+ echo ""
+
+ # Check if already installed
+ if kubectl get deployment cos-s3-csi-controller -n kube-system >/dev/null 2>&1; then
+ echo "✓ CSI driver already installed"
+
+ # Check version
+ CURRENT_IMAGE=$(kubectl get deployment cos-s3-csi-controller -n kube-system \
+ -o jsonpath='{.spec.template.spec.containers[1].image}' 2>/dev/null || echo "unknown")
+
+ echo "✓ CSI driver is running with image: $CURRENT_IMAGE"
+ echo "Storage classes:"
+ kubectl get storageclass | grep cos-s3-csi || echo "No CSI storage classes found"
+ exit 0
+ fi
+
+ echo ""
+ echo "Creating temporary directory structure..."
+ mkdir -p /tmp/csi-driver
+ cd /tmp/csi-driver
+
+ # Extract manifests from ConfigMap
+ echo "Extracting CSI manifests from ConfigMap..."
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.kustomization\.yaml}' > kustomization.yaml
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.cos-s3-csi-controller\.yaml}' > cos-s3-csi-controller.yaml
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.cos-s3-csi-driver\.yaml}' > cos-s3-csi-driver.yaml
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.cos-s3-csidriver\.yaml}' > cos-s3-csidriver.yaml
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.cos-s3-csi-sc\.yaml}' > cos-s3-csi-sc.yaml
+ kubectl get configmap "$CONFIGMAP_NAME" -n "$NAMESPACE" -o jsonpath='{.data.cos-s3-csi-s3fs-sc\.yaml}' > cos-s3-csi-s3fs-sc.yaml
+
+ echo "✓ Manifests extracted"
+ echo ""
+
+ # Replace placeholders in storage class files
+ echo "Templating storage classes..."
+ sed -i "s|MINIO_ENDPOINT_PLACEHOLDER|$MINIO_ENDPOINT|g" cos-s3-csi-sc.yaml
+ sed -i "s|REGION_PLACEHOLDER|$REGION|g" cos-s3-csi-sc.yaml
+
+ sed -i "s|MINIO_ENDPOINT_PLACEHOLDER|$MINIO_ENDPOINT|g" cos-s3-csi-s3fs-sc.yaml
+ sed -i "s|REGION_PLACEHOLDER|$REGION|g" cos-s3-csi-s3fs-sc.yaml
+
+ echo "✓ Storage classes templated"
+ echo ""
+
+ # Verify files were created
+ echo "Verifying files..."
+ ls -la
+ echo ""
+
+ # Apply using kustomize
+ echo "=================================================="
+ echo "Applying CSI driver with kustomize..."
+ echo "=================================================="
+ kubectl apply -k .
+
+ # Wait for CSI driver pods to be ready
+ echo ""
+ echo "Waiting for CSI driver to be ready..."
+ echo "This may take up to 5 minutes..."
+
+ kubectl wait --for=condition=available --timeout=300s \
+ deployment/cos-s3-csi-controller -n kube-system || {
+ echo "ERROR: Timeout waiting for CSI controller"
+ echo "Checking controller status:"
+ kubectl get deployment cos-s3-csi-controller -n kube-system
+ kubectl describe deployment cos-s3-csi-controller -n kube-system
+ exit 1
+ }
+
+ kubectl wait --for=condition=ready --timeout=300s \
+ pod -l app=cos-s3-csi-driver -n kube-system || {
+ echo "ERROR: Timeout waiting for CSI driver pods"
+ echo "Checking driver pod status:"
+ kubectl get pods -n kube-system -l app=cos-s3-csi-driver
+ kubectl describe pods -n kube-system -l app=cos-s3-csi-driver
+ exit 1
+ }
+
+ echo ""
+ echo "=================================================="
+ echo "✓ CSI Driver Installation Complete!"
+ echo "=================================================="
+ echo ""
+ echo "Installed components:"
+ kubectl get deployment cos-s3-csi-controller -n kube-system
+ kubectl get daemonset cos-s3-csi-driver -n kube-system
+ kubectl get storageclass | grep cos-s3-csi
+ echo ""
+ echo "=================================================="
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ volumes:
+ - name: tmp
+ emptyDir: {}
+
+---
+# Job to restart CSI driver after MinIO CA certificate is created
+# This ensures the CSI driver picks up the new certificate for TLS verification
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: csi-driver-restart
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-restart
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-65"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 3
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-restart
+ spec:
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ restartPolicy: Never
+ containers:
+ - name: csi-restarter
+ image: bitnami/kubectl:latest
+ imagePullPolicy: IfNotPresent
+ command: ["/bin/bash"]
+ args:
+ - "-c"
+ - |
+ set -e
+
+ echo "=================================================="
+ echo "Restarting CSI Driver to pick up MinIO certificate"
+ echo "=================================================="
+
+ # Check if CSI driver is installed
+ if ! kubectl get deployment cos-s3-csi-controller -n kube-system >/dev/null 2>&1; then
+ echo "CSI driver not installed, skipping restart"
+ exit 0
+ fi
+
+ echo "Restarting CSI controller..."
+ kubectl rollout restart deployment/cos-s3-csi-controller -n kube-system
+
+ echo "Restarting CSI driver daemonset..."
+ kubectl rollout restart daemonset/cos-s3-csi-driver -n kube-system
+
+ echo ""
+ echo "Waiting for rollout to complete..."
+ kubectl rollout status deployment/cos-s3-csi-controller -n kube-system --timeout=180s
+ kubectl rollout status daemonset/cos-s3-csi-driver -n kube-system --timeout=180s
+
+ echo ""
+ echo "=================================================="
+ echo "✓ CSI Driver Restart Complete!"
+ echo "=================================================="
+
+{{- end }}
+{{- end }}
diff --git a/geospatial-studio/templates/geoserver-install-job.yaml b/geospatial-studio/templates/geoserver-install-job.yaml
new file mode 100644
index 00000000..50c010c5
--- /dev/null
+++ b/geospatial-studio/templates/geoserver-install-job.yaml
@@ -0,0 +1,273 @@
+{{- if .Values.global.geoserver.enabled }}
+
+# 1. ConfigMap for server.xml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: geoserver-server-xml
+ namespace: {{ .Values.global.namespace }}
+data:
+ server.xml: |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: geofm-geoserver-cm
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: geoserver
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+data:
+ CORS_ALLOWED_HEADERS: "Origin,Host,Cache-Control,Content-Security-Policy,X-Content-Type-Options,Access-Control-Allow-Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers"
+ CORS_ENABLED: "true"
+ EXTRA_JAVA_OPTS: {{ .Values.global.geoserver.javaOpts | default "-Xms4g -Xmx8g" | quote }}
+ GEOSERVER_CSRF_WHITELIST: ".local"
+ INSTALL_EXTENSIONS: "true"
+ PROXY_BASE_URL: "http://geofm-geoserver.{{ .Values.global.namespace }}.svc.cluster.local:3000/geoserver"
+ RUN_UNPRIVILEGED: "true"
+ SKIP_DEMO_DATA: "true"
+ STABLE_EXTENSIONS: "wmts-multi-dimensional,netcdf-out,netcdf"
+
+---
+# PersistentVolumeClaim for GeoServer data
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: gfm-geoserver-pvc
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: geoserver
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+ storageClassName: {{ .Values.global.geoserver.storageClass | default "local-path" }}
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: {{ .Values.global.geoserver.storage | default "60Gi" }}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: geofm-geoserver
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: gfm-geoserver
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: gfm-geoserver
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: gfm-geoserver
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ {{- if and .Values.global.imagePullSecret .Values.global.imagePullSecret.name }}
+ imagePullSecrets:
+ - name: {{ .Values.global.imagePullSecret.name }}
+ {{- end }}
+ securityContext:
+ {}
+ containers:
+ - name: gfm-geoserver
+ securityContext:
+ {}
+ image: {{ .Values.global.geoserver.image | default "docker.osgeo.org/geoserver:2.28.1" }}
+ imagePullPolicy: {{ .Values.global.imagePullPolicy }}
+ envFrom:
+ - configMapRef:
+ name: geofm-geoserver-cm
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ {{- if .Values.global.geoserver.resources }}
+ resources:
+ {{- toYaml .Values.global.geoserver.resources | nindent 22 }}
+ {{- end }}
+ volumeMounts:
+ - mountPath: /opt/geoserver_data
+ name: gfm-geoserver-pv-storage
+ - mountPath: /usr/local/tomcat/conf/server.xml
+ name: server-xml-config
+ subPath: server.xml
+ volumes:
+ - name: gfm-geoserver-pv-storage
+ persistentVolumeClaim:
+ claimName: gfm-geoserver-pvc
+ - name: server-xml-config
+ configMap:
+ name: geoserver-server-xml
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: geofm-geoserver
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: gfm-geoserver
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+spec:
+ type: ClusterIP
+ ports:
+ - port: 3000
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: gfm-geoserver
+ app.kubernetes.io/instance: {{ .Release.Name }}
+
+---
+# Job to configure GeoServer after it's running
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-geoserver-configurator
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: geoserver-configurator
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": post-install,post-upgrade
+ "helm.sh/hook-weight": "10"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 5
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: geoserver-configurator
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ containers:
+ - name: geoserver-configurator
+ image: curlimages/curl:latest
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: GEOSERVER_USERNAME
+ value: {{ .Values.global.geoserver.adminUsername | default "admin" | quote }}
+ - name: GEOSERVER_PASSWORD
+ value: {{ .Values.global.geoserver.adminPassword | default "geoserver" | quote }}
+ - name: GEOSERVER_URL
+ value: "http://geofm-geoserver.{{ .Values.global.namespace }}.svc.cluster.local:3000/geoserver"
+ command:
+ - /bin/sh
+ - -c
+ - |
+ set -e
+
+ echo "=================================================="
+ echo "GeoServer Configuration Job"
+ echo "=================================================="
+ echo "GeoServer URL: $GEOSERVER_URL"
+ echo "=================================================="
+
+ # Wait for GeoServer to be ready
+ echo "Waiting for GeoServer to accept connections..."
+ MAX_RETRIES=60
+ RETRY_COUNT=0
+
+ until curl --silent --show-error -u $GEOSERVER_USERNAME:$GEOSERVER_PASSWORD -f $GEOSERVER_URL/rest/workspaces > /dev/null 2>&1; do
+ RETRY_COUNT=$((RETRY_COUNT + 1))
+ if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
+ echo "ERROR: GeoServer did not become ready after $MAX_RETRIES attempts"
+ exit 1
+ fi
+ echo "Attempt $RETRY_COUNT/$MAX_RETRIES: GeoServer not ready yet, waiting..."
+ sleep 10
+ done
+
+ echo "✓ GeoServer is ready!"
+
+ # Create workspace
+ echo ""
+ echo "Creating 'geofm' workspace..."
+ curl --silent --show-error -u $GEOSERVER_USERNAME:$GEOSERVER_PASSWORD -L -X POST $GEOSERVER_URL/rest/workspaces \
+ --header "Content-type: text/xml" \
+ --data 'geofm' || echo "Workspace might already exist"
+ echo "✓ Workspace created"
+
+ # Update WMS Settings
+ echo ""
+ echo "Updating WMS settings..."
+ curl --silent --show-error -u $GEOSERVER_USERNAME:$GEOSERVER_PASSWORD -L -X PUT $GEOSERVER_URL/rest/services/wms/settings \
+ --header "Content-type: application/json" \
+ --data '{"wms": {"maxBuffer": 25, "maxRequestMemory": 65536, "maxRenderingTime": 60, "maxRenderingErrors": 1000, "metadata": {"entry": [{"@key": "svgRenderer", "$": "Batik"},{"@key":"svgAntiAlias","$":"true"}]}}}'
+ echo "✓ WMS settings updated"
+
+ # Allow services
+ echo ""
+ echo "Configuring service ACLs..."
+ curl --silent --show-error -u $GEOSERVER_USERNAME:$GEOSERVER_PASSWORD -L -X POST $GEOSERVER_URL/rest/security/acl/services \
+ --header "Content-type: application/json" \
+ --data '{"gwc.*":"*","wcs.*":"*","wfs.*":"*","wms.*":"*"}' || echo "ACLs might already be configured"
+ echo "✓ Service ACLs configured"
+
+ echo ""
+ echo "=================================================="
+ echo "GeoServer configuration completed successfully!"
+ echo "=================================================="
+{{- end }}
diff --git a/geospatial-studio/templates/keycloak-install-job.yaml b/geospatial-studio/templates/keycloak-install-job.yaml
new file mode 100644
index 00000000..ff419eed
--- /dev/null
+++ b/geospatial-studio/templates/keycloak-install-job.yaml
@@ -0,0 +1,367 @@
+{{- if .Values.global.keycloak.enabled }}
+---
+# Job to deploy Keycloak before other components
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-keycloak-installer
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: keycloak-installer
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-80"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 5
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: keycloak-installer
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ {{- if .Values.global.imagePullSecret }}
+ imagePullSecrets:
+ - name: {{ .Values.global.imagePullSecret.name }}
+ {{- end }}
+ containers:
+ - name: keycloak-installer
+ image: bitnami/kubectl:latest
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -e
+
+ echo "=================================================="
+ echo "Keycloak Installation Job"
+ echo "=================================================="
+ echo "Release Name: {{ .Release.Name }}"
+ echo "Namespace: {{ .Values.global.namespace }}"
+ echo "Keycloak Image: {{ .Values.global.keycloak.image }}:{{ .Values.global.keycloak.tag }}"
+ echo "=================================================="
+
+ # Create Keycloak Deployment
+ cat </dev/null 2>&1; do
+ echo "Keycloak not ready yet, retrying in 5 seconds..."
+ sleep 5
+ done
+ echo "Keycloak is ready!"
+
+ # Get admin access token
+ echo "Getting admin access token..."
+ KC_TOKEN=$(curl -s --request POST --url "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
+ --header 'content-type: application/x-www-form-urlencoded' \
+ --data client_id=admin-cli \
+ --data grant_type=password \
+ --data username={{ .Values.global.keycloak.auth.adminUser }} \
+ --data password={{ .Values.global.keycloak.auth.adminPassword }} | jq -r '.access_token')
+
+ if [ -z "$KC_TOKEN" ] || [ "$KC_TOKEN" = "null" ]; then
+ echo "ERROR: Failed to get admin access token"
+ exit 1
+ fi
+ echo "Access token obtained successfully"
+
+ # Create realm
+ echo "Creating realm '{{ .Values.global.keycloak.realm }}'..."
+ curl -s -L -X POST "$KEYCLOAK_URL/admin/realms" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" \
+ --data '{"realm": "{{ .Values.global.keycloak.realm }}", "enabled": true}' || echo "Realm may already exist"
+
+ # Create client
+ echo "Creating client 'geostudio-client'..."
+ CLUSTER_URL="{{ .Values.global.cluster_url }}"
+ NAMESPACE="{{ .Values.global.namespace }}"
+
+ curl -s -L -X POST "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/clients" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" \
+ --data '{
+ "clientId": "geostudio-client",
+ "enabled": true,
+ "clientAuthenticatorType": "client-secret",
+ "secret": "'"${CLIENT_SECRET}"'",
+ "redirectUris": [
+ "https://geofm-ui.'"${NAMESPACE}"'.svc.cluster.local:4180/oauth2/callback",
+ "https://geofm-gateway.'"${NAMESPACE}"'.svc.cluster.local:4180/oauth2/callback",
+ "https://localhost:4180/oauth2/callback",
+ "https://localhost:4181/oauth2/callback",
+ "https://geofm-ui-'"${NAMESPACE}"'.'"${CLUSTER_URL}"'/oauth2/callback",
+ "https://geofm-gateway-'"${NAMESPACE}"'.'"${CLUSTER_URL}"'/oauth2/callback",
+ "http://localhost:3000/*"
+ ],
+ "webOrigins": ["*"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": true,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": true,
+ "publicClient": false,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "oidc.ciba.grant.enabled": "false",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "backchannel.logout.session.required": "true",
+ "backchannel.logout.revoke.offline.tokens": "false"
+ }
+ }' || echo "Client may already exist"
+
+ # Get client UUID
+ echo "Getting client UUID..."
+ CLIENT_UUID=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/clients?clientId=geostudio-client" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" | jq -r '.[0].id')
+
+ echo "Client UUID: $CLIENT_UUID"
+
+ # Verify client secret
+ ACTUAL_SECRET=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/clients/$CLIENT_UUID/client-secret" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" | jq -r '.value')
+
+ echo "Client Secret configured: $ACTUAL_SECRET"
+
+ # Create test user
+ echo "Creating test user '{{ .Values.global.keycloak.testUser.username }}'..."
+ curl -s -L -X POST "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/users" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" \
+ --data '{
+ "username": "{{ .Values.global.keycloak.testUser.username }}",
+ "email": "{{ .Values.global.keycloak.testUser.email }}",
+ "enabled": true,
+ "firstName": "{{ .Values.global.keycloak.testUser.firstName }}",
+ "lastName": "{{ .Values.global.keycloak.testUser.lastName }}",
+ "emailVerified": true
+ }' || echo "User may already exist"
+
+ # Get user ID
+ echo "Getting user ID..."
+ USER_ID=$(curl -s -X GET "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/users?username={{ .Values.global.keycloak.testUser.username }}" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" | jq -r '.[0].id')
+
+ echo "User ID: $USER_ID"
+
+ # Set user password
+ echo "Setting user password..."
+ curl -s -L -X PUT "$KEYCLOAK_URL/admin/realms/{{ .Values.global.keycloak.realm }}/users/$USER_ID/reset-password" \
+ --header "Content-Type: application/json" \
+ --header "Authorization: Bearer $KC_TOKEN" \
+ --data '{
+ "type": "password",
+ "value": "{{ .Values.global.keycloak.testUser.password }}",
+ "temporary": false
+ }'
+
+ echo ""
+ echo "=================================================="
+ echo "Keycloak configuration completed successfully!"
+ echo "=================================================="
+ echo "Realm: {{ .Values.global.keycloak.realm }}"
+ echo "Client ID: geostudio-client"
+ echo "Client Secret: $CLIENT_SECRET"
+ echo "Test User: {{ .Values.global.keycloak.testUser.username }}"
+ echo "Test Password: {{ .Values.global.keycloak.testUser.password }}"
+ echo "=================================================="
+ echo "Issuer URL: http://keycloak.{{ .Values.global.namespace }}.svc.cluster.local:8080/realms/{{ .Values.global.keycloak.realm }}"
+ echo "Auth URL: http://keycloak.{{ .Values.global.namespace }}.svc.cluster.local:8080/realms/{{ .Values.global.keycloak.realm }}/protocol/openid-connect/auth"
+ echo "=================================================="
+{{- end }}
diff --git a/geospatial-studio/templates/minio-ca-configmaps.yaml b/geospatial-studio/templates/minio-ca-configmaps.yaml
new file mode 100644
index 00000000..36fe6076
--- /dev/null
+++ b/geospatial-studio/templates/minio-ca-configmaps.yaml
@@ -0,0 +1,154 @@
+{{- if and (eq .Values.global.objectStorage.cos_storage_class "cos-s3-csi-s3fs-sc") (.Values.global.objectStorage.createCosSecret) }}
+{{- if or (.Values.global.minio.enabled) (contains "minio" .Values.global.objectStorage.endpoint) }}
+{{- if .Values.global.objectStorage.tlsCertPem }}
+---
+# ConfigMap with MinIO CA certificate for CSI driver in kube-system namespace
+# Uses manually provided certificate from values
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: minio-public-config
+ namespace: kube-system
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: csi-driver-config
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-75"
+data:
+ minio-public.crt: |
+{{ .Values.global.objectStorage.tlsCertPem | toString | nindent 4 }}
+---
+# ConfigMap with MinIO CA certificate for application namespace
+# Uses manually provided certificate from values
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: minio-public-config
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: storage-config
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-75"
+data:
+ minio-public.crt: |
+{{ .Values.global.objectStorage.tlsCertPem | toString | nindent 4 }}
+{{- else if .Values.global.minio.enabled }}
+---
+# Job to create ConfigMaps from auto-generated minio-tls-secret
+# Runs after certificate generation job completes
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: minio-ca-configmap-creator
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: minio-ca-configmap-creator
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-70"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 5
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: minio-ca-configmap-creator
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ containers:
+ - name: configmap-creator
+ image: bitnami/kubectl:latest
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -e
+
+ echo "Waiting for minio-tls-secret to be created..."
+ for i in {1..30}; do
+ if kubectl get secret minio-tls-secret -n {{ .Values.global.namespace }} >/dev/null 2>&1; then
+ echo "Secret found!"
+ break
+ fi
+ if [ $i -eq 30 ]; then
+ echo "ERROR: minio-tls-secret not found after 30 attempts"
+ exit 1
+ fi
+ echo "Attempt $i/30: Secret not found yet, waiting..."
+ sleep 2
+ done
+
+ echo "Extracting certificate from secret..."
+ CERT=$(kubectl get secret minio-tls-secret -n {{ .Values.global.namespace }} -o jsonpath='{.data.tls\.crt}' | base64 -d)
+
+ if [ -z "$CERT" ]; then
+ echo "ERROR: Failed to extract certificate"
+ exit 1
+ fi
+
+ echo "Creating ConfigMap in kube-system namespace..."
+ kubectl create configmap minio-public-config \
+ --from-literal=minio-public.crt="$CERT" \
+ --namespace=kube-system \
+ --dry-run=client -o yaml | \
+ kubectl label -f - --local --dry-run=client -o yaml \
+ app.kubernetes.io/name=geostudio \
+ app.kubernetes.io/component=csi-driver-config \
+ app.kubernetes.io/managed-by={{ .Release.Service }} | \
+ kubectl apply -f -
+
+ echo "Creating ConfigMap in {{ .Values.global.namespace }} namespace..."
+ kubectl create configmap minio-public-config \
+ --from-literal=minio-public.crt="$CERT" \
+ --namespace={{ .Values.global.namespace }} \
+ --dry-run=client -o yaml | \
+ kubectl label -f - --local --dry-run=client -o yaml \
+ app.kubernetes.io/name=geostudio \
+ app.kubernetes.io/component=storage-config \
+ app.kubernetes.io/managed-by={{ .Release.Service }} | \
+ kubectl apply -f -
+
+ echo "ConfigMaps created successfully in both namespaces"
+
+ {{- if eq .Values.global.objectStorage.cos_storage_class "cos-s3-csi-s3fs-sc" }}
+ echo ""
+ echo "=================================================="
+ echo "Restarting S3 CSI controller to pick up certificate..."
+ echo "=================================================="
+
+ # Find and restart CSI controller pod
+ CSI_POD=$(kubectl get pods -n kube-system --no-headers | grep 'cos-s3-csi-controller' | awk '{print $1}' | head -1)
+
+ if [ -n "$CSI_POD" ]; then
+ echo "Found CSI controller pod: $CSI_POD"
+ kubectl delete pod $CSI_POD -n kube-system
+ echo "✓ CSI controller restart triggered"
+
+ # Wait a bit for new pod to start
+ sleep 5
+ echo "Waiting for new CSI controller to be ready..."
+ for i in {1..30}; do
+ NEW_POD=$(kubectl get pods -n kube-system --no-headers | grep 'cos-s3-csi-controller' | grep 'Running' | awk '{print $1}' | head -1)
+ if [ -n "$NEW_POD" ]; then
+ echo "✓ New CSI controller pod ready: $NEW_POD"
+ break
+ fi
+ sleep 2
+ done
+ else
+ echo "Note: S3 CSI controller not found. Skipping restart."
+ fi
+ echo "=================================================="
+ {{- end }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/geospatial-studio/templates/minio-cert-job.yaml b/geospatial-studio/templates/minio-cert-job.yaml
new file mode 100644
index 00000000..18c642b7
--- /dev/null
+++ b/geospatial-studio/templates/minio-cert-job.yaml
@@ -0,0 +1,130 @@
+{{- if .Values.global.minio.enabled }}
+---
+# Job to generate self-signed TLS certificate for MinIO
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: tls-cert-generator
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: tls-generator
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-85"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 3
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: tls-generator
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ containers:
+ - name: cert-generator
+ image: bitnami/kubectl:latest
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -e
+
+ # Check if TLS secrets already exist
+ if kubectl get secret geofm-gateway-authentication-tls -n {{ .Values.global.namespace }} &>/dev/null && \
+ kubectl get secret geofm-ui-authentication-tls -n {{ .Values.global.namespace }} &>/dev/null && \
+ kubectl get secret minio-tls-secret -n {{ .Values.global.namespace }} &>/dev/null; then
+ echo "✓ TLS secrets already exist, skipping generation"
+ exit 0
+ fi
+
+ # Install openssl
+ echo "Installing openssl..."
+ apt-get update && apt-get install -y openssl
+
+ echo "Generating self-signed TLS certificate for MinIO..."
+
+ # Create OpenSSL config
+ cat > /tmp/openssl.conf </dev/null; do
+ echo "MinIO not ready yet, retrying in 5 seconds..."
+ sleep 5
+ done
+ echo "MinIO is ready!"
+
+ # Function to create bucket if it doesn't exist
+ create_bucket() {
+ local bucket_name=$1
+ echo "Checking if bucket '$bucket_name' exists..."
+
+ if mc ls myminio/$bucket_name --insecure >/dev/null 2>&1; then
+ echo "Bucket '$bucket_name' already exists, skipping creation"
+ else
+ echo "Creating bucket '$bucket_name'..."
+ mc mb myminio/$bucket_name --insecure
+ echo "Bucket '$bucket_name' created successfully"
+ fi
+ }
+
+ # Create GeoStudio buckets
+ echo ""
+ echo "Creating GeoStudio buckets..."
+ {{- range $key, $value := .Values.global.objectStorage.buckets }}
+ create_bucket "{{ $value }}"
+ {{- end }}
+
+ echo ""
+ echo "=================================================="
+ echo "Bucket creation completed successfully!"
+ echo "=================================================="
+ echo "Created buckets:"
+ {{- range $key, $value := .Values.global.objectStorage.buckets }}
+ echo " - {{ $value }}"
+ {{- end }}
+ echo "=================================================="
+
+ # List all buckets for verification
+ echo ""
+ echo "All buckets in MinIO:"
+ mc ls myminio --insecure
+{{- end }}
diff --git a/geospatial-studio/templates/postgresql-install-job.yaml b/geospatial-studio/templates/postgresql-install-job.yaml
new file mode 100644
index 00000000..cd37443e
--- /dev/null
+++ b/geospatial-studio/templates/postgresql-install-job.yaml
@@ -0,0 +1,246 @@
+{{- if .Values.global.postgresql.enabled }}
+---
+# Job to deploy PostgreSQL using Helm before other components
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-postgresql-installer
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: postgresql-installer
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-100"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 5
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: postgresql-installer
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ restartPolicy: Never
+ serviceAccountName: {{ .Values.global.serviceAccount.name }}
+ {{- if .Values.global.imagePullSecret }}
+ imagePullSecrets:
+ - name: {{ .Values.global.imagePullSecret.name }}
+ {{- end }}
+ containers:
+ - name: postgresql-installer
+ image: dtzar/helm-kubectl:latest
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -e
+
+ echo "=================================================="
+ echo "PostgreSQL Installation Job"
+ echo "=================================================="
+ echo "Release Name: {{ .Release.Name }}"
+ echo "Namespace: {{ .Values.global.namespace }}"
+ echo "Chart Version: {{ .Values.global.postgresql.chartVersion }}"
+ echo "=================================================="
+
+ # Add Bitnami Helm repository
+ echo "Adding Bitnami Helm repository..."
+ helm repo add bitnami https://charts.bitnami.com/bitnami
+ helm repo update
+
+ # Check if PostgreSQL pod is already running
+ if kubectl get pod -n {{ .Values.global.namespace }} -l app.kubernetes.io/name=postgresql,app.kubernetes.io/instance={{ .Values.global.postgresql.fullnameOverride }} 2>/dev/null | grep -q Running; then
+ echo "✓ PostgreSQL pod is already running"
+ kubectl get pod -n {{ .Values.global.namespace }} -l app.kubernetes.io/name=postgresql,app.kubernetes.io/instance={{ .Values.global.postgresql.fullnameOverride }}
+ echo "Skipping installation"
+ exit 0
+ fi
+
+ # Check if PostgreSQL Helm release exists
+ if helm list -n {{ .Values.global.namespace }} | grep -q "^{{ .Values.global.postgresql.fullnameOverride }}\s"; then
+ echo "PostgreSQL release '{{ .Values.global.postgresql.fullnameOverride }}' exists but pod not running"
+ echo "Upgrading PostgreSQL..."
+ helm upgrade {{ .Values.global.postgresql.fullnameOverride }} bitnami/postgresql \
+ --namespace {{ .Values.global.namespace }} \
+ --version {{ .Values.global.postgresql.chartVersion }} \
+ --set image.tag=latest \
+ --set fullnameOverride={{ .Values.global.postgresql.fullnameOverride }} \
+ --set auth.username={{ .Values.global.postgresql.auth.username }} \
+ --set auth.password={{ .Values.global.postgresql.auth.password }} \
+ --set auth.database={{ .Values.global.postgresql.auth.database }} \
+ --set primary.persistence.enabled={{ .Values.global.postgresql.primary.persistence.enabled }} \
+ --set primary.persistence.size={{ .Values.global.postgresql.primary.persistence.size }} \
+ {{- if .Values.global.postgresql.primary.persistence.storageClass }}
+ --set primary.persistence.storageClass={{ .Values.global.postgresql.primary.persistence.storageClass }} \
+ {{- end }}
+ --set primary.resources.requests.memory={{ .Values.global.postgresql.primary.resources.requests.memory }} \
+ --set primary.resources.requests.cpu={{ .Values.global.postgresql.primary.resources.requests.cpu }} \
+ --set primary.resources.limits.memory={{ .Values.global.postgresql.primary.resources.limits.memory }} \
+ --set primary.resources.limits.cpu={{ .Values.global.postgresql.primary.resources.limits.cpu }} \
+ --wait \
+ --timeout 10m
+ else
+ echo "Installing PostgreSQL chart..."
+ helm install {{ .Values.global.postgresql.fullnameOverride }} bitnami/postgresql \
+ --namespace {{ .Values.global.namespace }} \
+ --version {{ .Values.global.postgresql.chartVersion }} \
+ --set image.tag=latest \
+ --set fullnameOverride={{ .Values.global.postgresql.fullnameOverride }} \
+ --set auth.username={{ .Values.global.postgresql.auth.username }} \
+ --set auth.password={{ .Values.global.postgresql.auth.password }} \
+ --set auth.database={{ .Values.global.postgresql.auth.database }} \
+ --set primary.persistence.enabled={{ .Values.global.postgresql.primary.persistence.enabled }} \
+ --set primary.persistence.size={{ .Values.global.postgresql.primary.persistence.size }} \
+ {{- if .Values.global.postgresql.primary.persistence.storageClass }}
+ --set primary.persistence.storageClass={{ .Values.global.postgresql.primary.persistence.storageClass }} \
+ {{- end }}
+ --set primary.resources.requests.memory={{ .Values.global.postgresql.primary.resources.requests.memory }} \
+ --set primary.resources.requests.cpu={{ .Values.global.postgresql.primary.resources.requests.cpu }} \
+ --set primary.resources.limits.memory={{ .Values.global.postgresql.primary.resources.limits.memory }} \
+ --set primary.resources.limits.cpu={{ .Values.global.postgresql.primary.resources.limits.cpu }} \
+ --wait \
+ --timeout 10m
+ fi
+
+ echo "PostgreSQL deployment completed successfully"
+
+ # Wait for PostgreSQL to be ready
+ echo "Waiting for PostgreSQL to be ready..."
+ kubectl wait --for=condition=ready pod \
+ -l app.kubernetes.io/name=postgresql \
+ -n {{ .Values.global.namespace }} \
+ --timeout=600s
+
+ echo "PostgreSQL is ready!"
+
+ # Display PostgreSQL connection info
+ echo "=================================================="
+ echo "PostgreSQL Connection Information:"
+ echo "Host: {{ .Values.global.postgresql.fullnameOverride }}.{{ .Values.global.namespace }}.svc.cluster.local"
+ echo "Port: 5432"
+ echo "Username: {{ .Values.global.postgresql.auth.username }}"
+ echo "Default Database: {{ .Values.global.postgresql.auth.database }}"
+ echo "=================================================="
+
+ echo "Installation job completed successfully"
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ - name: helm-cache
+ mountPath: /.cache/helm
+ - name: helm-config
+ mountPath: /.config/helm
+ volumes:
+ - name: tmp
+ emptyDir: {}
+ - name: helm-cache
+ emptyDir: {}
+ - name: helm-config
+ emptyDir: {}
+---
+# Job to create GeoStudio databases after PostgreSQL is ready
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ .Release.Name }}-postgresql-db-creator
+ namespace: {{ .Values.global.namespace }}
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: postgresql-db-creator
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-90"
+ "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+ backoffLimit: 10
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: geostudio
+ app.kubernetes.io/component: postgresql-db-creator
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ restartPolicy: Never
+ {{- if .Values.global.imagePullSecret }}
+ imagePullSecrets:
+ - name: {{ .Values.global.imagePullSecret.name }}
+ {{- end }}
+ containers:
+ - name: db-creator
+ image: postgres:latest
+ command:
+ - /bin/bash
+ - -c
+ - |
+ set -e
+
+ echo "=================================================="
+ echo "PostgreSQL Database Creation Job"
+ echo "=================================================="
+ echo "PostgreSQL Host: {{ .Values.global.postgresql.fullnameOverride }}.{{ .Values.global.namespace }}.svc.cluster.local"
+ echo "=================================================="
+
+ export PGPASSWORD="{{ .Values.global.postgresql.auth.password }}"
+ export PGHOST="{{ .Values.global.postgresql.fullnameOverride }}.{{ .Values.global.namespace }}.svc.cluster.local"
+ export PGPORT="5432"
+ export PGUSER="{{ .Values.global.postgresql.auth.username }}"
+
+ # Wait for PostgreSQL to be ready
+ echo "Waiting for PostgreSQL to accept connections..."
+ until pg_isready -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -t 3; do
+ echo "PostgreSQL not ready yet, retrying in 5 seconds..."
+ sleep 5
+ done
+ echo "PostgreSQL is accepting connections!"
+
+ # Function to create database if it doesn't exist
+ create_database() {
+ local db_name=$1
+ echo "Checking if database '$db_name' exists..."
+
+ DB_EXISTS=$(psql -tA -c "SELECT 1 FROM pg_database WHERE datname='$db_name'")
+ if [ "$DB_EXISTS" = "1" ]; then
+ echo "Database '$db_name' already exists, skipping creation"
+ else
+ echo "Creating database '$db_name'..."
+ psql -c "CREATE DATABASE $db_name;"
+ echo "Database '$db_name' created successfully"
+ fi
+ }
+
+ # Create GeoStudio databases
+ echo ""
+ echo "Creating GeoStudio databases..."
+ create_database "geostudio"
+ create_database "geostudio_auth"
+ create_database "mlflow"
+ {{- if .Values.global.keycloak.enabled }}
+ create_database "{{ .Values.global.keycloak.database }}"
+ {{- end }}
+
+ echo ""
+ echo "=================================================="
+ echo "Database creation completed successfully!"
+ echo "=================================================="
+ echo "Created databases:"
+ echo " - geostudio"
+ echo " - geostudio_auth"
+ echo " - mlflow"
+ {{- if .Values.global.keycloak.enabled }}
+ echo " - {{ .Values.global.keycloak.database }}"
+ {{- end }}
+ echo "=================================================="
+
+ # List all databases for verification
+ echo ""
+ echo "All databases in PostgreSQL:"
+ psql -t -c "SELECT datname FROM pg_database WHERE datistemplate = false;"
+{{- end }}
diff --git a/geospatial-studio/templates/serviceaccount.yaml b/geospatial-studio/templates/serviceaccount.yaml
index 0fed4cdf..c8a6eb98 100644
--- a/geospatial-studio/templates/serviceaccount.yaml
+++ b/geospatial-studio/templates/serviceaccount.yaml
@@ -8,18 +8,26 @@ kind: ServiceAccount
metadata:
name: {{ .Values.global.serviceAccount.name }}
namespace: {{ .Values.global.namespace }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-110"
+ "helm.sh/hook-delete-policy": before-hook-creation
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: {{ .Values.global.namespace }}
name: {{ .Values.global.serviceAccount.name }}-role
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-110"
+ "helm.sh/hook-delete-policy": before-hook-creation
rules:
- apiGroups: [""]
resources: ["configmaps", "pods", "pods/exec", "secrets", "services", "serviceaccounts", "persistentvolumeclaims"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: ["apps"]
- resources: ["deployments", "replicasets"]
+ resources: ["deployments", "replicasets", "statefulsets"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: ["route.openshift.io"]
resources: ["routes"]
@@ -27,17 +35,114 @@ rules:
- apiGroups: ["batch"]
resources: ["jobs","jobs/status"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["networking.k8s.io"]
+ resources: ["networkpolicies"]
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["rbac.authorization.k8s.io"]
+ resources: ["roles", "rolebindings"]
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["policy"]
+ resources: ["poddisruptionbudgets"]
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+{{- if .Values.global.minio.enabled }}
+- apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["get", "list", "delete"]
+ # For CSI driver restart in kube-system
+- apiGroups: [""]
+ resources: ["configmaps"]
+ verbs: ["get", "list"]
+{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ .Values.global.serviceAccount.name }}-rb
namespace: {{ .Values.global.namespace }}
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-110"
+ "helm.sh/hook-delete-policy": before-hook-creation
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ .Values.global.serviceAccount.name }}-role
subjects:
+- kind: ServiceAccount
+ name: {{ .Values.global.serviceAccount.name }}
+ namespace: {{ .Values.global.namespace }}
+---
+# ClusterRole for cross-namespace operations (e.g., CSI driver restart in kube-system)
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ .Values.global.serviceAccount.name }}-cluster-role
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-110"
+ "helm.sh/hook-delete-policy": before-hook-creation
+rules:
+- apiGroups: [""]
+ resources: ["pods", "configmaps"]
+ verbs: ["get", "list", "delete", "create", "update", "patch"]
+ # For CSI driver operations and ConfigMap creation in kube-system
+{{- if .Values.global.csiDriver.enabled }}
+- apiGroups: [""]
+ resources: ["serviceaccounts"]
+ verbs: ["create", "get", "list", "patch", "update"]
+ # For CSI driver ServiceAccount creation
+{{- end }}
+- apiGroups: ["apps"]
+ resources: ["deployments", "replicasets"]
+ verbs: ["get", "list"]
+{{- if .Values.global.csiDriver.enabled }}
+- apiGroups: ["apps"]
+ resources: ["deployments", "daemonsets"]
+ verbs: ["create", "get", "list", "patch", "update"]
+ # For CSI driver Deployment and DaemonSet creation
+- apiGroups: ["rbac.authorization.k8s.io"]
+ resources: ["clusterroles", "clusterrolebindings"]
+ verbs: ["create", "get", "list", "patch", "update", "escalate", "bind"]
+ # For CSI driver RBAC setup - escalate and bind allow creating roles with more permissions
+- apiGroups: ["storage.k8s.io"]
+ resources: ["storageclasses", "csidrivers", "csinodes", "volumeattachments"]
+ verbs: ["create", "get", "list", "patch", "update", "watch"]
+ # For CSI driver StorageClass, CSIDriver, and related resources
+- apiGroups: [""]
+ resources: ["persistentvolumes", "persistentvolumeclaims"]
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+ # For CSI driver PV/PVC management
+- apiGroups: [""]
+ resources: ["events"]
+ verbs: ["create", "get", "list", "patch", "update", "watch"]
+ # For CSI driver event creation
+- apiGroups: [""]
+ resources: ["nodes"]
+ verbs: ["get", "list", "watch", "update", "patch"]
+ # For CSI driver node operations and node labeling
+- apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "list"]
+ # For CSI driver secret access
+- apiGroups: [""]
+ resources: ["namespaces"]
+ verbs: ["get", "list"]
+ # For CSI driver namespace discovery
+{{- end }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ .Values.global.serviceAccount.name }}-cluster-rb
+ annotations:
+ "helm.sh/hook": pre-install,pre-upgrade
+ "helm.sh/hook-weight": "-110"
+ "helm.sh/hook-delete-policy": before-hook-creation
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ .Values.global.serviceAccount.name }}-cluster-role
+subjects:
- kind: ServiceAccount
name: {{ .Values.global.serviceAccount.name }}
namespace: {{ .Values.global.namespace }}
diff --git a/geospatial-studio/values.yaml b/geospatial-studio/values.yaml
index f7ee1c80..bc193b78 100644
--- a/geospatial-studio/values.yaml
+++ b/geospatial-studio/values.yaml
@@ -26,7 +26,7 @@ global:
imagePullSecret:
name: us-icr-pull-secret
b64secret: image_pull_secret_b64
- create: true
+ create: false
jira:
api_key: ''
@@ -51,7 +51,7 @@ global:
responseType: responseType
# oauth2-proxy needs
issuerUrl: OAUTH_ISSUER_URL # authorization_endpoint used in oauth2-proxy
- clientSecret: clientSecret
+ clientSecret: oauth_client_secret
cookieSecret: cookieSecret
extraOauthProxyArgs: OAUTH_EXTRA_PROXY_ARGS
# for kubernetes we need the tls secret
@@ -121,10 +121,10 @@ global:
port: 6432
cesium:
- token: token
+ token: cesium_token
mapbox:
- token: token
+ token: mapbox_token
appNames:
gateway: geofm-gateway
@@ -142,6 +142,135 @@ global:
password: redis_password
architecture: REDIS_ARCHITECTURE
+ # ==============================================================================
+ # Infrastructure Resources
+ # ==============================================================================
+
+ # PostgreSQL Configuration
+ # Deployed via pre-install Helm hook job when enabled
+ # The job runs with hook-weight -100 to ensure PostgreSQL is ready before other components
+ # Two jobs are created:
+ # 1. postgresql-installer (weight: -100): Deploys PostgreSQL using Bitnami Helm chart
+ # 2. postgresql-db-creator (weight: -90): Creates required databases (geostudio, geostudio_auth, mlflow)
+ postgresql:
+ enabled: false
+ chartVersion: "latest"
+ fullnameOverride: "postgresql"
+ auth:
+ username: pg_username
+ password: pg_password
+ database: postgres
+ primary:
+ persistence:
+ enabled: true
+ size: 10Gi
+ storageClass: ""
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 250m
+ limits:
+ memory: 1Gi
+ cpu: 1000m
+
+ # MinIO Configuration
+ # Deployed via pre-install Helm hook jobs when enabled
+ # Jobs run with hook-weight -80 and -70
+ # Three jobs are created:
+ # 1. minio-tls-cert-generator (weight: -85): Generates self-signed TLS certificate
+ # 2. minio-installer (weight: -80): Deploys MinIO with TLS
+ # 3. minio-bucket-creator (weight: -70): Creates required S3 buckets
+ minio:
+ enabled: false
+ image: quay.io/minio/minio
+ tag: "latest"
+ auth:
+ rootUser: access_key_id
+ rootPassword: secret_access_key
+ persistence:
+ enabled: true
+ size: 100Gi
+ storageClass: local-path
+ resources:
+ requests:
+ memory: 2Gi
+ cpu: 2000m
+ limits:
+ memory: 4Gi
+ cpu: 4000m
+
+ # Keycloak Configuration
+ # Deployed via pre-install Helm hook job when enabled
+ # The job runs with hook-weight -80 (after PostgreSQL) to ensure Keycloak is ready before other components
+ # Two jobs are created:
+ # 1. keycloak-installer (weight: -80): Deploys Keycloak with PostgreSQL backend
+ # 2. keycloak-configurator (weight: -70): Creates realm, client, and test user
+ keycloak:
+ enabled: false
+ image: quay.io/keycloak/keycloak
+ tag: "26.4.5"
+ database: keycloak
+ realm: geostudio
+ clientSecret: oauth_client_secret
+ auth:
+ adminUser: keycloak_admin_user
+ adminPassword: keycloak_admin_password
+ testUser:
+ username: testuser
+ password: testpass123
+ email: test@example.com
+ firstName: Test
+ lastName: User
+ resources:
+ requests:
+ memory: 1Gi
+ cpu: 500m
+ limits:
+ memory: 2Gi
+ cpu: 1000m
+
+ # GeoServer Configuration
+ # Deployed via pre-install Helm hook jobs when enabled
+ # Jobs run with hook-weight -55 and -50
+ # Three components are created:
+ # 1. Secret with server.xml (weight: -60): PostgreSQL JNDI datasource configuration
+ # 2. geoserver-installer (weight: -55): Deploys GeoServer with configured resources
+ # 3. geoserver-configurator (weight: -50): Creates workspace and configures WMS settings
+ # Note: Requires PostgreSQL to be running (depends on postgresql-installer job)
+ geoserver:
+ enabled: false
+ image: "docker.osgeo.org/geoserver:2.28.1"
+ adminUsername: geoserver_username
+ adminPassword: geoserver_password
+ javaOpts: "-Xms4g -Xmx8g"
+ storage: "60Gi"
+ storageClass: "local-path"
+ resources:
+ requests:
+ memory: 4Gi
+ cpu: 1000m
+ limits:
+ memory: 8Gi
+ cpu: 2000m
+
+ # CSI Driver Configuration
+ # IBM Object S3 CSI driver for MinIO/S3 bucket mounting as PersistentVolumes
+ # Deployed via pre-install Helm hook job when enabled
+ # The job runs with hook-weight -75 (after TLS cert creation, before bucket creation)
+ #
+ # Available type:
+ # - "ibm-object-s3-csi": IBM Object S3 CSI driver (x86_64 only)
+ # Creates storage classes: cos-s3-csi-sc and cos-s3-csi-s3fs-sc
+ csiDriver:
+ enabled: false
+ type: "ibm-object-s3-csi"
+ images:
+ # IBM Object S3 CSI images
+ provisioner: "k8s.gcr.io/sig-storage/csi-provisioner:v5.2.0"
+ driver: "quay.io/containerstorage/ibm-object-csi-driver:v0.1.19"
+ nodeDriverRegistrar: "k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.13.0"
+ livenessProbe: "registry.k8s.io/sig-storage/livenessprobe:v2.16.0"
+
### Sub Charts Specific Values ###
# gfm-studio-gateway:
@@ -149,9 +278,17 @@ gfm-studio-gateway:
enabled: true
resources:
api:
+ requests: {}
+ limits: {}
oauth:
+ requests: {}
+ limits: {}
celeryWorker:
+ requests: {}
+ limits: {}
celeryFlower:
+ requests: {}
+ limits: {}
image:
name: quay.io/geospatial-studio/geostudio-gateway
tag: latest
@@ -221,7 +358,11 @@ geofm-ui:
enabled: true
resources:
ui:
+ requests: {}
+ limits: {}
oauth:
+ requests: {}
+ limits: {}
image:
name: quay.io/geospatial-studio/geostudio-ui
tag: latest
@@ -241,6 +382,8 @@ geofm-ui:
gfm-mlflow:
enabled: true
resources:
+ requests: {}
+ limits: {}
image:
name: ghcr.io/mlflow/mlflow
# pullPolicy inherited from global.imagePullPolicy
@@ -554,6 +697,7 @@ geospatial-studio-pipelines:
operator: In
values:
- NODE_GPU_SPEC
+
- name: generic-python-processor
enabled: true
image:
diff --git a/geostudio b/geostudio
new file mode 100755
index 00000000..9c0dd497
--- /dev/null
+++ b/geostudio
@@ -0,0 +1,332 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio CLI - Unified Command Interface
+# ==============================================================================
+# Unified command-line interface for managing GeoStudio operator and applications
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+#
+# Usage:
+# ./geostudio [subcommand] [options]
+#
+# Commands:
+# operator Manage the GeoStudio operator (infrastructure)
+# app Manage GeoStudio application instances
+# help Show detailed help
+#
+# Examples:
+# ./geostudio operator install --local
+# ./geostudio app deploy
+# ./geostudio app status --namespace prod
+# ==============================================================================
+
+set -euo pipefail
+
+# ==============================================================================
+# Initialize
+# ==============================================================================
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$SCRIPT_DIR"
+LIB_DIR="$SCRIPT_DIR/lib"
+
+# Source shared libraries
+source "$LIB_DIR/common.sh"
+source "$LIB_DIR/k8s-utils.sh"
+
+# Script version
+CLI_VERSION="0.1.0"
+
+# Global options
+DRY_RUN=false
+VERBOSE=false
+
+# ==============================================================================
+# Help Functions
+# ==============================================================================
+
+show_main_help() {
+ cat << 'EOF'
+GeoStudio CLI - Unified Management Interface
+
+USAGE:
+ geostudio [subcommand] [options]
+
+COMMANDS:
+ build Build operator image for local or production use
+ operator Manage the GeoStudio operator (infrastructure)
+ app Manage GeoStudio application instances
+ help Show this help message
+ version Show CLI version
+
+BUILD OPTIONS:
+ --local Build for local development (Lima, kind, or k8s)
+ --prod Build and push to quay.io registry
+ --version Specify version tag (for production builds)
+
+OPERATOR SUBCOMMANDS:
+ install Install operator [--local|--prod] [--version VERSION]
+ uninstall Remove operator and CRDs [--force] [--skip-csi-cleanup]
+ status Check operator health and readiness
+ logs View operator logs [--follow]
+ restart Restart operator deployment
+
+APP SUBCOMMANDS:
+ deploy Deploy application instance [--env ENV] [--namespace NS]
+ delete Remove application instance [--namespace NS] [--keep-pvcs]
+ list List all deployed instances
+ status Check application health [--namespace NS]
+ logs View application logs [--namespace NS] [--component NAME]
+ restart Restart application pods [--namespace NS]
+
+GLOBAL OPTIONS:
+ --dry-run Show what would happen without executing
+ --verbose Enable verbose output
+ --help Show help for command
+
+EXAMPLES:
+ # Development workflow
+ ./geostudio build --local
+ ./geostudio operator install --local
+ ./geostudio app deploy
+
+ # Production workflow
+ ./geostudio build --prod --version v1.0.0
+ ./geostudio operator install --prod --version v1.0.0
+ ./geostudio app deploy --env production --namespace prod
+
+ # Check status
+ ./geostudio operator status
+ ./geostudio app status --namespace prod
+
+ # View logs
+ ./geostudio operator logs --follow
+ ./geostudio app logs --namespace prod --component gateway
+
+ # Clean uninstall workflow (recommended order)
+ ./geostudio app delete --namespace staging
+ ./geostudio app delete --namespace default
+ ./geostudio operator uninstall
+
+For more information, visit: https://github.com/geospatial-studio/geospatial-studio
+EOF
+}
+
+show_operator_help() {
+ cat << 'EOF'
+GeoStudio CLI - Operator Management
+
+USAGE:
+ geostudio operator [options]
+
+SUBCOMMANDS:
+ install Install the GeoStudio operator
+ uninstall Remove the GeoStudio operator and CRDs
+ status Check operator health and readiness
+ logs View operator logs
+ restart Restart the operator deployment
+
+INSTALL OPTIONS:
+ --local Use locally built image (Lima, kind, or k8s)
+ --prod Use production image from quay.io
+ --version VERSION Specify operator version (default: latest for prod, local for local)
+ --namespace NS Operator namespace (default: geostudio-operators-system)
+
+UNINSTALL OPTIONS:
+ --namespace NS Operator namespace (default: geostudio-operators-system)
+ --force Force uninstall even if app instances exist (not recommended)
+ --skip-csi-cleanup Skip CSI driver cleanup (for advanced scenarios)
+
+STATUS OPTIONS:
+ --namespace NS Operator namespace (default: geostudio-operators-system)
+
+LOGS OPTIONS:
+ --namespace NS Operator namespace (default: geostudio-operators-system)
+ --follow, -f Follow log output
+
+RESTART OPTIONS:
+ --namespace NS Operator namespace (default: geostudio-operators-system)
+
+EXAMPLES:
+ # Install for local development (auto-detects Lima, kind, or k8s)
+ geostudio operator install --local
+
+ # Install for specific cluster type
+ CLUSTER_TYPE=kind geostudio operator install --local
+
+ # Install for production
+ geostudio operator install --prod --version v0.1.0
+
+ # Check operator status
+ geostudio operator status
+
+ # View live logs
+ geostudio operator logs --follow
+
+ # Restart operator
+ geostudio operator restart
+
+ # Uninstall operator (blocks if apps exist)
+ geostudio operator uninstall
+
+ # Force uninstall (advanced - may leave orphaned resources)
+ geostudio operator uninstall --force
+
+ENVIRONMENT VARIABLES:
+ CLUSTER_TYPE Override cluster type detection (lima|kind|k8s)
+ KIND_CLUSTER_NAME Name of kind cluster (default: kind)
+
+SUPPORTED CLUSTERS:
+ Lima Local Lima VM with k3s (https://github.com/lima-vm/lima)
+ kind Kubernetes in Docker (https://kind.sigs.k8s.io/)
+ k8s Native Kubernetes cluster
+EOF
+}
+
+show_app_help() {
+ cat << 'EOF'
+GeoStudio CLI - Application Management
+
+USAGE:
+ geostudio app [options]
+
+SUBCOMMANDS:
+ deploy Deploy a GeoStudio application instance
+ delete Remove a GeoStudio application instance
+ list List all deployed GeoStudio instances
+ status Check application health
+ logs View application logs
+ restart Restart application pods
+
+DEPLOY OPTIONS:
+ --env ENV Deployment environment (default: lima)
+ --namespace NS Target namespace (default: default)
+ --dry-run Generate manifest without applying
+
+DELETE OPTIONS:
+ --namespace NS Target namespace (default: default)
+ --keep-pvcs Don't delete PersistentVolumeClaims
+
+LIST OPTIONS:
+ --namespace NS Filter by namespace (default: all namespaces)
+
+STATUS OPTIONS:
+ --namespace NS Target namespace (default: default)
+
+LOGS OPTIONS:
+ --namespace NS Target namespace (default: default)
+ --component NAME Filter by component (gateway, ui, mlflow, etc.)
+ --follow, -f Follow log output
+
+RESTART OPTIONS:
+ --namespace NS Target namespace (default: default)
+ --component NAME Restart specific component (default: all)
+
+EXAMPLES:
+ # Deploy to lima/default
+ ./geostudio app deploy
+
+ # Deploy to production
+ ./geostudio app deploy --env production --namespace prod
+
+ # List all instances
+ ./geostudio app list
+
+ # Check app status
+ ./geostudio app status --namespace prod
+
+ # View gateway logs
+ ./geostudio app logs --namespace prod --component gateway --follow
+
+ # Restart UI component
+ ./geostudio app restart --namespace prod --component ui
+
+ # Delete instance
+ ./geostudio app delete --namespace staging
+EOF
+}
+
+# ==============================================================================
+# Command Router
+# ==============================================================================
+
+main() {
+ # Check for no arguments
+ if [ $# -eq 0 ]; then
+ show_main_help
+ exit 0
+ fi
+
+ # Parse global options first
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --dry-run)
+ DRY_RUN=true
+ export DRY_RUN
+ shift
+ ;;
+ --verbose)
+ VERBOSE=true
+ set -x
+ shift
+ ;;
+ --help)
+ show_main_help
+ exit 0
+ ;;
+ version|--version)
+ echo "GeoStudio CLI version $CLI_VERSION"
+ exit 0
+ ;;
+ help)
+ show_main_help
+ exit 0
+ ;;
+ build)
+ shift
+ source "$LIB_DIR/build-commands.sh"
+ build_command "$@"
+ exit $?
+ ;;
+ operator)
+ shift
+ source "$LIB_DIR/operator-commands.sh"
+ operator_command "$@"
+ exit $?
+ ;;
+ app)
+ shift
+ source "$LIB_DIR/app-commands.sh"
+ app_command "$@"
+ exit $?
+ ;;
+ *)
+ log_error "Unknown command: $1"
+ echo ""
+ echo "Run './geostudio help' for usage information"
+ exit 1
+ ;;
+ esac
+ done
+}
+
+# ==============================================================================
+# Trap Handlers
+# ==============================================================================
+
+cleanup() {
+ # Clean up any temporary files
+ if [ -n "${TEMP_MANIFEST:-}" ] && [ -f "$TEMP_MANIFEST" ]; then
+ rm -f "$TEMP_MANIFEST" 2>/dev/null || true
+ fi
+}
+
+trap cleanup EXIT
+trap 'log_error "Script interrupted"; exit 130' INT TERM
+
+# ==============================================================================
+# Entry Point
+# ==============================================================================
+
+main "$@"
diff --git a/lib/app-commands.sh b/lib/app-commands.sh
new file mode 100644
index 00000000..b8d19d76
--- /dev/null
+++ b/lib/app-commands.sh
@@ -0,0 +1,665 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio Application Commands
+# ==============================================================================
+# Application management functions (deploy, delete, list, status, logs, restart)
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+# ==============================================================================
+
+# Source dependencies
+if [ -z "$GREEN" ]; then
+ source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
+ source "$(dirname "${BASH_SOURCE[0]}")/k8s-utils.sh"
+fi
+
+# ==============================================================================
+# App Command Router
+# ==============================================================================
+
+app_command() {
+ if [ $# -eq 0 ]; then
+ show_app_help
+ exit 0
+ fi
+
+ local subcommand=$1
+ shift
+
+ case $subcommand in
+ deploy)
+ app_deploy "$@"
+ ;;
+ delete)
+ app_delete "$@"
+ ;;
+ list)
+ app_list "$@"
+ ;;
+ status)
+ app_status "$@"
+ ;;
+ logs)
+ app_logs "$@"
+ ;;
+ restart)
+ app_restart "$@"
+ ;;
+ --help|-h|help)
+ show_app_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown app subcommand: $subcommand"
+ echo ""
+ echo "Run 'geostudio app help' for usage"
+ exit 1
+ ;;
+ esac
+}
+
+# ==============================================================================
+# App Deploy
+# ==============================================================================
+
+app_deploy() {
+ export DEPLOYMENT_ENV=${DEPLOYMENT_ENV:-lima}
+ export OC_PROJECT=${OC_PROJECT:-default}
+ local dry_run_local=false
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --env)
+ export DEPLOYMENT_ENV="$2"
+ shift 2
+ ;;
+ --namespace)
+ export OC_PROJECT="$2"
+ shift 2
+ ;;
+ --dry-run)
+ dry_run_local=true
+ DRY_RUN=true
+ shift
+ ;;
+ --help|-h)
+ show_app_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ show_app_help
+ exit 1
+ ;;
+ esac
+ done
+
+ # File paths
+ local workspace_dir="$PROJECT_ROOT/workspace/$DEPLOYMENT_ENV"
+ local env_file="$workspace_dir/env/.env"
+ local env_sh_file="$workspace_dir/env/env.sh"
+ local template_file="$PROJECT_ROOT/operators/examples/geostudio-operator-template.yaml"
+ local output_file="$workspace_dir/geostudio-operator-deploy.yaml"
+ local studio_api_key_file="$PROJECT_ROOT/.studio-api-key"
+
+ log_step "GEOStudio Application Deployment"
+ echo "Environment: $DEPLOYMENT_ENV"
+ echo "Namespace: $OC_PROJECT"
+ echo "Workspace: $workspace_dir"
+ echo ""
+
+ # Check if operator is installed
+ log_info "Checking if GEOStudio operator is installed..."
+ if ! operator_is_installed; then
+ echo ""
+ log_error "GEOStudio operator is not installed!"
+ echo ""
+ echo "The GEOStudio operator must be installed before deploying applications."
+ echo ""
+ log_info "To install the operator, run:"
+ echo " geostudio operator install --local"
+ echo ""
+ exit 1
+ fi
+
+ if ! operator_is_running; then
+ log_warning "Operator is not ready"
+ echo ""
+ log_info "Check operator status:"
+ echo " geostudio operator status"
+ echo ""
+ if ! confirm "Continue anyway?"; then
+ log_info "Aborted"
+ exit 1
+ fi
+ else
+ log_success "GEOStudio operator is installed and running"
+ fi
+ echo ""
+
+ # Check for envsubst
+ if ! command -v envsubst &> /dev/null; then
+ log_error "envsubst not found. Please install it:"
+ echo ""
+ echo " macOS: brew install gettext && brew link --force gettext"
+ echo " Linux: sudo apt-get install gettext-base"
+ echo ""
+ exit 1
+ fi
+
+ # Step 1: Setup workspace environment
+ log_step "Setting up workspace"
+
+ if [ ! -f "$PROJECT_ROOT/deployment-scripts/setup-workspace-env.sh" ]; then
+ log_error "setup-workspace-env.sh not found"
+ exit 1
+ fi
+
+ # Set Lima-specific defaults before running setup
+ export ROUTE_ENABLED=false
+
+ # Run the workspace setup script
+ cd "$PROJECT_ROOT"
+ ./deployment-scripts/setup-workspace-env.sh
+
+ # Step 2: Apply cluster-specific configuration overrides
+ local cluster_type=$(get_cluster_type)
+ log_step "Applying configuration for $cluster_type cluster"
+
+ # Generate OAuth cookie secret
+ export cookie_secret=$(cat /dev/urandom | base64 | tr -dc '0-9a-zA-Z' | head -c32)
+
+ # Use consistent OAuth client secret that matches Keycloak configuration
+ # This must match the value in geospatial-studio/values.yaml (global.keycloak.clientSecret)
+ export oauth_client_secret="oauth_client_secret"
+
+ # Generate TLS certificates for local development
+ log_info "Generating TLS certificates for local development..."
+ local tls_dir="$workspace_dir/tls"
+ mkdir -p "$tls_dir"
+
+ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
+ -keyout "$tls_dir/tls.key" \
+ -out "$tls_dir/tls.crt" \
+ -subj "/CN=$OC_PROJECT.svc.cluster.local" 2>/dev/null
+
+ # Extract the cert and key into base64 env vars
+ export TLS_CRT_B64=$(openssl base64 -in "$tls_dir/tls.crt" -A)
+ export TLS_KEY_B64=$(openssl base64 -in "$tls_dir/tls.key" -A)
+
+ # Generate dummy image pull secret (for local development)
+ # This is a valid base64 encoded dockerconfigjson with dummy credentials
+ export IMAGE_PULL_SECRET_B64="eyJhdXRocyI6eyJleGFtcGxlLmlvIjp7InVzZXJuYW1lIjoiZXhhbXBsZSIsInBhc3N3b3JkIjoiZXhhbXBsZSIsImVtYWlsIjoiZXhhbXBsZUBleGFtcGxlLmNvbSIsImF1dGgiOiJaWGhoYlhCc1pUcGxlR0Z0Y0d4bCJ9fX0="
+
+ # Determine non-COS storage class based on cluster type
+ local non_cos_storage_class="standard" # Default for kind, k8s, nvkind, openshift
+ if [ "$cluster_type" = "lima" ]; then
+ non_cos_storage_class="local-path"
+ fi
+
+ # Apply configuration using cross-platform sed
+ sed_inplace "$env_sh_file" 's|export ROUTE_ENABLED=.*|export ROUTE_ENABLED=false|g'
+ sed_inplace "$env_sh_file" "s/export ENVIRONMENT=.*/export ENVIRONMENT=local/g"
+ sed_inplace "$env_sh_file" "s/export CLUSTER_URL=.*/export CLUSTER_URL=localhost/g"
+
+ # Determine CSI driver type and storage class
+ local csi_driver_type=$(get_csi_driver_type)
+ local cos_storage_class="cos-s3-csi-s3fs-sc"
+ log_info "Using CSI driver: $csi_driver_type with storage class: $cos_storage_class"
+
+ sed_inplace "$env_sh_file" "s/export COS_STORAGE_CLASS=.*/export COS_STORAGE_CLASS=$cos_storage_class/g"
+ sed_inplace "$env_sh_file" "s/export CSI_DRIVER_TYPE=.*/export CSI_DRIVER_TYPE=$csi_driver_type/g"
+ sed_inplace "$env_sh_file" "s/export NON_COS_STORAGE_CLASS=.*/export NON_COS_STORAGE_CLASS=$non_cos_storage_class/g"
+ sed_inplace "$env_sh_file" "s/export SHARE_PIPELINE_PVC=.*/export SHARE_PIPELINE_PVC=true/g"
+ sed_inplace "$env_sh_file" "s/export STORAGE_PVC_ENABLED=.*/export STORAGE_PVC_ENABLED=true/g"
+ sed_inplace "$env_sh_file" "s/export STORAGE_FILESYSTEM_ENABLED=.*/export STORAGE_FILESYSTEM_ENABLED=false/g"
+ sed_inplace "$env_sh_file" "s/export CREATE_TUNING_FOLDERS_FLAG=.*/export CREATE_TUNING_FOLDERS_FLAG=false/g"
+ sed_inplace "$env_sh_file" "s|export PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE=.*|export PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE=/data|g"
+ sed_inplace "$env_sh_file" "s/export OAUTH_TYPE=.*/export OAUTH_TYPE=keycloak/g"
+ sed_inplace "$env_sh_file" "s/export OAUTH_CLIENT_ID=.*/export OAUTH_CLIENT_ID=geostudio-client/g"
+ sed_inplace "$env_sh_file" "s|export OAUTH_ISSUER_URL=.*|export OAUTH_ISSUER_URL=http://keycloak.$OC_PROJECT.svc.cluster.local:8080/realms/geostudio|g"
+ sed_inplace "$env_sh_file" "s/export OAUTH_PROXY_PORT=.*/export OAUTH_PROXY_PORT=4180/g"
+ sed_inplace "$env_sh_file" "s/export CREATE_TLS_SECRET=.*/export CREATE_TLS_SECRET=false/g"
+
+ # Set default credentials for local development (MinIO, PostgreSQL, Keycloak)
+ sed_inplace "$env_file" "s/access_key_id=.*/access_key_id=minioadmin/g"
+ sed_inplace "$env_file" "s/secret_access_key=.*/secret_access_key=minioadmin/g"
+ sed_inplace "$env_file" "s/oauth_client_secret=.*/oauth_client_secret=$oauth_client_secret/g"
+ sed_inplace "$env_file" "s/oauth_cookie_secret=.*/oauth_cookie_secret=$cookie_secret/g"
+ sed_inplace "$env_file" "s|endpoint=.*|endpoint=https://minio.$OC_PROJECT.svc.cluster.local:9000|g"
+ sed_inplace "$env_file" "s/region=.*/region=us-east-1/g"
+ sed_inplace "$env_file" "s/pg_username=.*/pg_username=postgres/g"
+ sed_inplace "$env_file" "s/pg_password=.*/pg_password=devPostgresql123/g"
+ sed_inplace "$env_file" "s/pg_uri=.*/pg_uri=postgresql.$OC_PROJECT.svc.cluster.local/g"
+ sed_inplace "$env_file" "s/pg_port=.*/pg_port=5432/g"
+ sed_inplace "$env_file" "s/pg_studio_db_name=.*/pg_studio_db_name=geostudio/g"
+ sed_inplace "$env_file" "s/geoserver_username=.*/geoserver_username=admin/g"
+ sed_inplace "$env_file" "s/geoserver_password=.*/geoserver_password=geoserver/g"
+ sed_inplace "$env_file" "s/image_pull_policy=.*/image_pull_policy=IfNotPresent/g"
+ sed_inplace "$env_file" "s|tls_crt_b64=.*|tls_crt_b64=$TLS_CRT_B64|g"
+ sed_inplace "$env_file" "s|tls_key_b64=.*|tls_key_b64=$TLS_KEY_B64|g"
+ sed_inplace "$env_file" "s|image_pull_secret_b64=.*|image_pull_secret_b64=$IMAGE_PULL_SECRET_B64|g"
+
+ # Set dummy tokens for Mapbox and Cesium (required for secret creation, even if not used)
+ # Using "none" as placeholder - these will be base64 encoded by Helm
+ sed_inplace "$env_file" "s/mapbox_token=.*/mapbox_token=none/g"
+ sed_inplace "$env_file" "s/cesium_token=.*/cesium_token=none/g"
+
+ # Set dummy values for optional API keys (SentinelHub, NASA, Jira)
+ sed_inplace "$env_file" "s/sh_client_id=.*/sh_client_id=none/g"
+ sed_inplace "$env_file" "s/sh_client_secret=.*/sh_client_secret=none/g"
+ sed_inplace "$env_file" "s/nasa_earth_data_bearer_token=.*/nasa_earth_data_bearer_token=none/g"
+ sed_inplace "$env_file" "s/jira_api_key=.*/jira_api_key=none/g"
+
+ log_success "Configuration applied for $cluster_type"
+
+ # Step 3: Merge .studio-api-key if it exists
+ log_step "Checking for .studio-api-key"
+ if [ -f "$studio_api_key_file" ]; then
+ log_success "Found $studio_api_key_file"
+ source "$studio_api_key_file"
+
+ # Update studio_api_key if it exists in .env
+ if grep -q "studio_api_key=" "$env_file" 2>/dev/null; then
+ sed_inplace "$env_file" "s|studio_api_key=.*|studio_api_key=${STUDIO_API_KEY}|g"
+ fi
+
+ # Update studio_api_encryption_key if it exists in .env
+ if grep -q "studio_api_encryption_key=" "$env_file" 2>/dev/null; then
+ sed_inplace "$env_file" "s|studio_api_encryption_key=.*|studio_api_encryption_key=${API_ENCRYPTION_KEY}|g"
+ fi
+ else
+ log_info "(not found - will use values from .env)"
+ fi
+
+ # Step 4: Source environment files
+ log_step "Loading environment configuration"
+
+ if [ ! -f "$env_file" ] || [ ! -f "$env_sh_file" ]; then
+ log_error "Environment files not found"
+ exit 1
+ fi
+
+ log_success "Sourcing $env_file"
+ set -a
+ source "$env_file"
+ set +a
+
+ log_success "Sourcing $env_sh_file"
+ source "$env_sh_file"
+
+ # Validate critical variables are set
+ log_info "Validating required environment variables..."
+ local missing_vars=()
+
+ # Check for critical variables
+ [ -z "${pg_username:-}" ] && missing_vars+=("pg_username")
+ [ -z "${pg_password:-}" ] && missing_vars+=("pg_password")
+ [ -z "${pg_uri:-}" ] && missing_vars+=("pg_uri")
+ [ -z "${pg_port:-}" ] && missing_vars+=("pg_port")
+ [ -z "${access_key_id:-}" ] && missing_vars+=("access_key_id")
+ [ -z "${secret_access_key:-}" ] && missing_vars+=("secret_access_key")
+ [ -z "${endpoint:-}" ] && missing_vars+=("endpoint")
+ [ -z "${region:-}" ] && missing_vars+=("region")
+ [ -z "${tls_crt_b64:-}" ] && missing_vars+=("tls_crt_b64")
+ [ -z "${tls_key_b64:-}" ] && missing_vars+=("tls_key_b64")
+ [ -z "${image_pull_secret_b64:-}" ] && missing_vars+=("image_pull_secret_b64")
+
+ if [ ${#missing_vars[@]} -gt 0 ]; then
+ log_error "Missing required environment variables:"
+ for var in "${missing_vars[@]}"; do
+ echo " - $var"
+ done
+ echo ""
+ log_error "Please check your environment files:"
+ echo " - $env_file"
+ echo " - $env_sh_file"
+ exit 1
+ fi
+
+ log_success "All required variables validated"
+
+ # Step 5: Generate operator CR from template
+ log_step "Generating GEOStudio operator CR"
+
+ if [ ! -f "$template_file" ]; then
+ log_error "Template file not found: $template_file"
+ exit 1
+ fi
+
+ log_info "Template: $(basename $template_file)"
+ log_info "Output: $output_file"
+
+ envsubst < "$template_file" > "$output_file"
+
+ log_success "Generated: $output_file"
+
+ # Step 6: Apply to Kubernetes cluster
+ log_step "Applying to Kubernetes cluster"
+
+ if [ "$dry_run_local" = true ] || [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would apply manifest:"
+ echo " kubectl apply -f $output_file"
+ echo ""
+ log_info "Generated manifest saved to: $output_file"
+ return 0
+ fi
+
+ kubectl apply -f "$output_file"
+
+ echo ""
+ log_success "GEOStudio Deployment Submitted"
+ echo ""
+ log_info "Monitor deployment status:"
+ echo " geostudio app status"
+ echo " kubectl get pods -n $OC_PROJECT -w"
+ echo ""
+ log_info "View operator logs:"
+ echo " geostudio operator logs --follow"
+}
+
+# ==============================================================================
+# App Delete
+# ==============================================================================
+
+app_delete() {
+ local namespace="default"
+ local keep_pvcs=false
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --keep-pvcs)
+ keep_pvcs=true
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_step "Deleting GeoStudio Application"
+ echo "Namespace: $namespace"
+ echo ""
+
+ # Check if any instances exist
+ local instance_count=$(kubectl get geostudios -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ')
+
+ if [ "$instance_count" -eq 0 ]; then
+ log_info "No GeoStudio instances found in namespace: $namespace"
+
+ # Check if there are any other resources
+ local pod_count=$(kubectl get pods -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ')
+ local pvc_count=$(kubectl get pvc -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ')
+
+ if [ "$pod_count" -gt 0 ] || [ "$pvc_count" -gt 0 ]; then
+ echo ""
+ log_warning "Found remaining resources in namespace:"
+ [ "$pod_count" -gt 0 ] && echo " - $pod_count pods"
+ [ "$pvc_count" -gt 0 ] && echo " - $pvc_count PVCs"
+ echo ""
+
+ if confirm "Delete all resources in namespace $namespace?"; then
+ log_info "Deleting all resources..."
+
+ # Delete deployments and statefulsets
+ kubectl delete deployments --all -n "$namespace" --timeout=10s 2>/dev/null || true
+ kubectl delete statefulsets --all -n "$namespace" --timeout=10s 2>/dev/null || true
+
+ # Force delete pods
+ kubectl delete pods --all -n "$namespace" --force --grace-period=0 2>/dev/null || true
+
+ # Delete services, configmaps, secrets, jobs
+ kubectl delete services,configmaps,secrets,jobs --all -n "$namespace" 2>/dev/null || true
+
+ # Wait before deleting PVCs
+ if [ "$keep_pvcs" = false ] && [ "$pvc_count" -gt 0 ]; then
+ log_info "Waiting 10 seconds before deleting PVCs..."
+ sleep 10
+
+ log_info "Deleting PVCs..."
+ kubectl delete pvc --all -n "$namespace" --force --grace-period=0 2>/dev/null || true
+ sleep 2
+ fi
+
+ log_success "Resources deleted"
+ else
+ log_info "Keeping resources"
+ fi
+ else
+ log_info "No resources to delete"
+ fi
+
+ echo ""
+ log_success "Cleanup complete"
+ return 0
+ fi
+
+ # Show what will be deleted
+ log_info "Found $instance_count GeoStudio instance(s):"
+ kubectl get geostudios -n "$namespace"
+ echo ""
+
+ if ! confirm "This will delete the GeoStudio instance(s). Continue?"; then
+ log_info "Aborted"
+ exit 0
+ fi
+
+ log_info "Deleting GeoStudio instance(s) in namespace: $namespace"
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would delete GeoStudio instance(s)"
+ return 0
+ fi
+
+ # Delete GeoStudio instances
+ kubectl delete geostudios --all -n "$namespace" --timeout=20s 2>/dev/null || true
+
+ # Delete all application resources
+ log_info "Deleting application resources..."
+
+ # Delete deployments and statefulsets first
+ kubectl delete deployments --all -n "$namespace" --timeout=10s 2>/dev/null || true
+ kubectl delete statefulsets --all -n "$namespace" --timeout=10s 2>/dev/null || true
+
+ # Force delete any remaining pods
+ kubectl delete pods --all -n "$namespace" --force --grace-period=0 2>/dev/null || true
+
+ # Delete services, configmaps, secrets, jobs
+ kubectl delete services,configmaps,secrets,jobs --all -n "$namespace" 2>/dev/null || true
+
+ # Wait 10 seconds for resources to clean up before deleting PVCs
+ if [ "$keep_pvcs" = false ]; then
+ log_info "Waiting 10 seconds for resources to clean up before deleting PVCs..."
+ sleep 10
+
+ local pvc_count=$(kubectl get pvc -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ')
+ if [ "$pvc_count" -gt 0 ]; then
+ log_info "Deleting $pvc_count PVCs..."
+ kubectl delete pvc --all -n "$namespace" --force --grace-period=0 2>/dev/null || true
+
+ # Wait a bit for PVCs to start deleting
+ sleep 2
+
+ # Check if any are still stuck
+ local stuck_pvcs=$(kubectl get pvc -n "$namespace" --no-headers 2>/dev/null | wc -l | tr -d ' ')
+ if [ "$stuck_pvcs" -gt 0 ]; then
+ log_warning "$stuck_pvcs PVCs still terminating (this is normal)"
+ fi
+ else
+ log_info "No PVCs to delete"
+ fi
+ fi
+
+ log_success "GeoStudio instance deleted"
+
+ echo ""
+ log_info "Note: Shared cluster infrastructure remains installed:"
+ echo " - GeoStudio Operator"
+ echo " - IBM Object S3 CSI Driver"
+ echo ""
+ log_info "To remove all cluster infrastructure, run:"
+ echo " ./geostudio operator uninstall"
+}
+
+# ==============================================================================
+# App List
+# ==============================================================================
+
+app_list() {
+ local namespace=""
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_step "GeoStudio Instances"
+ echo ""
+
+ if [ -n "$namespace" ]; then
+ kubectl get geostudios -n "$namespace"
+ else
+ kubectl get geostudios --all-namespaces
+ fi
+}
+
+# ==============================================================================
+# App Status
+# ==============================================================================
+
+app_status() {
+ local namespace="default"
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_step "GeoStudio Application Status"
+ echo "Namespace: $namespace"
+ echo ""
+
+ # Check if any instances exist
+ if ! kubectl get geostudios -n "$namespace" &> /dev/null; then
+ log_error "No GeoStudio instances found in namespace: $namespace"
+ exit 1
+ fi
+
+ log_info "GeoStudio Resources:"
+ kubectl get geostudios -n "$namespace"
+ echo ""
+
+ log_info "Application Pods:"
+ kubectl get pods -n "$namespace" -l app.kubernetes.io/name=geostudio 2>/dev/null || echo "No pods found"
+ echo ""
+
+ log_info "Services:"
+ kubectl get svc -n "$namespace" -l app.kubernetes.io/name=geostudio 2>/dev/null || echo "No services found"
+}
+
+# ==============================================================================
+# App Logs
+# ==============================================================================
+
+app_logs() {
+ local namespace="default"
+ local component=""
+ local follow=false
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --component)
+ component="$2"
+ shift 2
+ ;;
+ --follow|-f)
+ follow=true
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ local selector="app.kubernetes.io/name=geostudio"
+ if [ -n "$component" ]; then
+ selector="${selector},app.kubernetes.io/component=${component}"
+ fi
+
+ if [ "$follow" = true ]; then
+ kubectl logs -n "$namespace" -l "$selector" -f
+ else
+ kubectl logs -n "$namespace" -l "$selector" --tail=100
+ fi
+}
+
+# ==============================================================================
+# App Restart
+# ==============================================================================
+
+app_restart() {
+ local namespace="default"
+ local component=""
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --component)
+ component="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_info "Restarting application pods in namespace: $namespace"
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would restart pods"
+ return 0
+ fi
+
+ if [ -n "$component" ]; then
+ kubectl rollout restart deployment -n "$namespace" -l "app.kubernetes.io/component=${component}"
+ else
+ kubectl rollout restart deployment -n "$namespace" -l "app.kubernetes.io/name=geostudio"
+ fi
+
+ log_success "Restart initiated"
+}
diff --git a/lib/build-commands.sh b/lib/build-commands.sh
new file mode 100644
index 00000000..c30a3133
--- /dev/null
+++ b/lib/build-commands.sh
@@ -0,0 +1,352 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio Build Commands
+# ==============================================================================
+# Build operator image for local or production use
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+# ==============================================================================
+
+# Source dependencies
+if [ -z "$GREEN" ]; then
+ source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
+ source "$(dirname "${BASH_SOURCE[0]}")/k8s-utils.sh"
+fi
+
+# ==============================================================================
+# Configuration
+# ==============================================================================
+
+IMAGE_NAME="geostudio-operator"
+QUAY_ORG="geospatial-studio"
+
+# ==============================================================================
+# Build Command Router
+# ==============================================================================
+
+build_command() {
+ if [ $# -eq 0 ]; then
+ show_build_help
+ exit 0
+ fi
+
+ local mode=""
+ local version=""
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --local)
+ mode="local"
+ shift
+ ;;
+ --prod|--production)
+ mode="prod"
+ shift
+ ;;
+ --version)
+ version="$2"
+ shift 2
+ ;;
+ --help|-h|help)
+ show_build_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ show_build_help
+ exit 1
+ ;;
+ esac
+ done
+
+ # Validate mode
+ if [ -z "$mode" ]; then
+ log_error "Build mode not specified. Use --local or --prod"
+ echo ""
+ show_build_help
+ exit 1
+ fi
+
+ # Execute build
+ if [ "$mode" = "local" ]; then
+ build_local
+ elif [ "$mode" = "prod" ]; then
+ build_prod "$version"
+ fi
+}
+
+# ==============================================================================
+# Local Build (Lima)
+# ==============================================================================
+
+build_local() {
+ local image_tag="local"
+ local full_image="${IMAGE_NAME}:${image_tag}"
+ local dockerfile="$PROJECT_ROOT/Dockerfile.operator.local"
+ local cluster_type=$(get_cluster_type)
+
+ log_step "Building Operator Image for Local Development"
+ echo "Cluster type: $cluster_type"
+ echo "Image: $full_image"
+ echo "Dockerfile: $(basename $dockerfile)"
+ echo ""
+
+ # Step 1: Build Docker image
+ log_info "1. Building Docker image..."
+ cd "$PROJECT_ROOT"
+ docker build --load -f "$dockerfile" -t "$full_image" .
+
+ # Step 2: Load image into cluster based on type
+ case "$cluster_type" in
+ lima)
+ load_image_to_lima "$full_image" "$image_tag"
+ ;;
+ kind)
+ load_image_to_kind "$full_image"
+ ;;
+ k8s)
+ load_image_to_k8s "$full_image"
+ ;;
+ *)
+ log_error "Unknown cluster type: $cluster_type"
+ exit 1
+ ;;
+ esac
+
+ echo ""
+ log_success "Image ready in $cluster_type cluster!"
+ echo ""
+ echo "Image: $full_image"
+ echo ""
+ log_info "Next steps:"
+ echo " ./geostudio operator install --local"
+}
+
+# Load image to Lima
+load_image_to_lima() {
+ local full_image=$1
+ local image_tag=$2
+
+ if ! command -v limactl &> /dev/null; then
+ log_error "limactl not found. Install Lima from https://github.com/lima-vm/lima"
+ exit 1
+ fi
+
+ # Step 2: Save image to tar
+ echo ""
+ log_info "2. Saving image to tar..."
+ docker save "$full_image" -o /tmp/${IMAGE_NAME}-${image_tag}.tar
+
+ # Step 3: Copy tar to Lima
+ echo ""
+ log_info "3. Copying image to Lima VM..."
+ limactl copy /tmp/${IMAGE_NAME}-${image_tag}.tar studio:/tmp/${IMAGE_NAME}-${image_tag}.tar
+
+ # Step 4: Import image in Lima
+ echo ""
+ log_info "4. Importing image into Lima containerd..."
+ limactl shell studio sudo ctr -n k8s.io images import /tmp/${IMAGE_NAME}-${image_tag}.tar
+
+ # Step 5: Verify image is available
+ echo ""
+ log_info "5. Verifying image in Lima..."
+ limactl shell studio sudo ctr -n k8s.io images ls | grep ${IMAGE_NAME}
+
+ # Cleanup
+ echo ""
+ log_info "6. Cleaning up temporary files..."
+ rm -f /tmp/${IMAGE_NAME}-${image_tag}.tar
+ limactl shell studio rm -f /tmp/${IMAGE_NAME}-${image_tag}.tar
+}
+
+# Load image to kind
+load_image_to_kind() {
+ local full_image=$1
+ local kind_cluster_name=${KIND_CLUSTER_NAME:-kind}
+
+ if ! command -v kind &> /dev/null; then
+ log_error "kind not found. Install kind from https://kind.sigs.k8s.io/"
+ exit 1
+ fi
+
+ # Auto-detect cluster name from kubectl context if not explicitly set
+ if [ -z "${KIND_CLUSTER_NAME:-}" ]; then
+ local current_context=$(kubectl config current-context 2>/dev/null || echo "")
+ if [[ "$current_context" == kind-* ]]; then
+ kind_cluster_name="${current_context#kind-}"
+ log_info "Auto-detected kind cluster name: $kind_cluster_name"
+ else
+ # If context doesn't start with kind-, try to get the first cluster
+ local first_cluster=$(kind get clusters 2>/dev/null | head -1)
+ if [ -n "$first_cluster" ]; then
+ kind_cluster_name="$first_cluster"
+ log_info "Using first available kind cluster: $kind_cluster_name"
+ else
+ log_error "No kind clusters found. Please create a cluster first:"
+ echo " kind create cluster --name studio"
+ exit 1
+ fi
+ fi
+ fi
+
+ echo ""
+ log_info "2. Loading image into kind cluster '$kind_cluster_name'..."
+ kind load docker-image "$full_image" --name "$kind_cluster_name"
+
+ echo ""
+ log_info "3. Verifying image in kind..."
+
+ # Use podman or docker depending on what's available
+ local container_runtime="docker"
+ if command -v podman &> /dev/null && [ -n "${KIND_EXPERIMENTAL_PROVIDER:-}" ]; then
+ container_runtime="podman"
+ fi
+
+ ${container_runtime} exec "${kind_cluster_name}-control-plane" crictl images 2>/dev/null | grep ${IMAGE_NAME} || true
+}
+
+# Load image to k8s
+load_image_to_k8s() {
+ local full_image=$1
+
+ log_warning "For native k8s clusters, you have the following options:"
+ echo ""
+ echo "1. Push to a registry and configure image pull:"
+ echo " docker tag $full_image /$full_image"
+ echo " docker push /$full_image"
+ echo ""
+ echo "2. Manually load on all nodes:"
+ echo " docker save $full_image | ssh node1 'docker load'"
+ echo ""
+ echo "3. Use production build instead:"
+ echo " ./geostudio build --prod --version "
+ echo ""
+ log_info "Image built locally: $full_image"
+ log_info "You'll need to make it available to your cluster nodes."
+}
+
+# ==============================================================================
+# Production Build (Quay.io)
+# ==============================================================================
+
+build_prod() {
+ local version=${1:-latest}
+ local full_image="quay.io/${QUAY_ORG}/${IMAGE_NAME}:${version}"
+ local dockerfile="$PROJECT_ROOT/Dockerfile.operator"
+
+ log_step "Building Operator Image for Production"
+ echo "Mode: production"
+ echo "Image: $full_image"
+ echo "Dockerfile: $(basename $dockerfile)"
+ echo ""
+
+ # Confirm push to production
+ log_warning "You are about to push to quay.io"
+ echo "Image: $full_image"
+ echo ""
+
+ if ! confirm "Are you sure you want to continue?"; then
+ log_info "Aborted"
+ exit 0
+ fi
+
+ # Check quay.io login
+ echo ""
+ # log_info "Checking quay.io login status..."
+ # if ! docker login quay.io --get-login > /dev/null 2>&1; then
+ # log_error "Not logged in to quay.io"
+ # echo ""
+ # echo "Please run: docker login quay.io"
+ # exit 1
+ # fi
+ # log_success "Logged in to quay.io"
+
+ # Build the image
+ echo ""
+ log_info "Building Docker image..."
+ cd "$PROJECT_ROOT"
+ docker build -f "$dockerfile" -t "$full_image" .
+
+ # Push to quay.io
+ echo ""
+ log_info "Pushing image to quay.io..."
+ docker push "$full_image"
+
+ echo ""
+ log_success "Image built and pushed to quay.io!"
+ echo ""
+ echo "Image: $full_image"
+ echo ""
+ log_info "Next steps:"
+ echo " ./geostudio operator install --prod --version $version"
+}
+
+# ==============================================================================
+# Help
+# ==============================================================================
+
+show_build_help() {
+ cat << 'EOF'
+GeoStudio CLI - Build Commands
+
+USAGE:
+ geostudio build [options]
+
+MODES:
+ --local Build for local development (Lima, kind, or k8s)
+ --prod Build and push to quay.io registry
+
+OPTIONS:
+ --version VERSION Specify version tag for production builds (default: latest)
+ --help, -h Show this help message
+
+ENVIRONMENT VARIABLES:
+ CLUSTER_TYPE Override cluster type detection (lima|kind|k8s)
+ KIND_CLUSTER_NAME Name of kind cluster (default: kind)
+
+EXAMPLES:
+ # Build for local development (auto-detects cluster type)
+ geostudio build --local
+
+ # Build for specific cluster type
+ CLUSTER_TYPE=kind geostudio build --local
+
+ # Build for production with version tag
+ geostudio build --prod --version v1.0.0
+
+ # Build for production (latest tag)
+ geostudio build --prod
+
+LOCAL BUILD WORKFLOW (Lima):
+ 1. Builds Docker image using Dockerfile.operator.local
+ 2. Saves image to tar file
+ 3. Copies tar to Lima VM
+ 4. Imports into Lima containerd
+ 5. Verifies image availability
+ 6. Cleans up temporary files
+
+LOCAL BUILD WORKFLOW (kind):
+ 1. Builds Docker image using Dockerfile.operator.local
+ 2. Loads image directly into kind cluster
+ 3. Verifies image availability
+
+LOCAL BUILD WORKFLOW (k8s):
+ 1. Builds Docker image using Dockerfile.operator.local
+ 2. Provides instructions for manual deployment
+
+PRODUCTION BUILD WORKFLOW:
+ 1. Confirms you want to push to production
+ 2. Checks quay.io authentication
+ 3. Builds Docker image using Dockerfile.operator
+ 4. Pushes to quay.io registry
+
+PREREQUISITES:
+ Lima: Docker, Lima (limactl), Lima VM named 'studio'
+ kind: Docker, kind CLI
+ k8s: Docker, access to cluster nodes or registry
+ Prod: Docker, authenticated to quay.io (docker login quay.io)
+
+EOF
+}
diff --git a/lib/common.sh b/lib/common.sh
new file mode 100644
index 00000000..1fcf7937
--- /dev/null
+++ b/lib/common.sh
@@ -0,0 +1,159 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio Common Library
+# ==============================================================================
+# Shared utilities and functions used across all GeoStudio CLI commands
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+# ==============================================================================
+
+# ==============================================================================
+# Color Constants
+# ==============================================================================
+
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+RED='\033[0;31m'
+YELLOW='\033[0;33m'
+NC='\033[0m' # No Color
+
+# ==============================================================================
+# Logging Functions
+# ==============================================================================
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+log_step() {
+ echo ""
+ echo -e "${BLUE}==>${NC} $1"
+}
+
+# ==============================================================================
+# User Interaction
+# ==============================================================================
+
+confirm() {
+ local prompt="$1"
+ if [ "$DRY_RUN" = true ]; then
+ return 0
+ fi
+ read -p "$prompt (y/N): " response
+ case "$response" in
+ [yY][eE][sS]|[yY])
+ return 0
+ ;;
+ *)
+ return 1
+ ;;
+ esac
+}
+
+execute() {
+ if [ "$DRY_RUN" = true ]; then
+ echo -e "${YELLOW}[DRY-RUN]${NC} $*"
+ else
+ "$@"
+ fi
+}
+
+# ==============================================================================
+# Script Path Resolution
+# ==============================================================================
+
+resolve_script_paths() {
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)"
+ PROJECT_ROOT="$SCRIPT_DIR/.."
+ LIB_DIR="$SCRIPT_DIR/lib"
+}
+
+# ==============================================================================
+# Prerequisites Checking
+# ==============================================================================
+
+require_command() {
+ local cmd=$1
+ local install_hint=${2:-""}
+
+ if ! command -v "$cmd" &> /dev/null; then
+ log_error "$cmd is not installed or not in PATH"
+ if [[ -n "$install_hint" ]]; then
+ echo ""
+ echo "$install_hint"
+ echo ""
+ fi
+ return 1
+ fi
+ return 0
+}
+
+check_kubectl_connection() {
+ if ! kubectl cluster-info &> /dev/null; then
+ log_error "Cannot connect to Kubernetes cluster"
+ log_error "Check your kubeconfig and cluster connection"
+ return 1
+ fi
+ return 0
+}
+
+# ==============================================================================
+# Kubernetes Utilities
+# ==============================================================================
+
+resource_exists() {
+ local resource_type=$1
+ local resource_name=$2
+ local namespace=${3:-""}
+
+ local ns_flag=""
+ [[ -n "$namespace" ]] && ns_flag="-n $namespace"
+
+ kubectl get "$resource_type" "$resource_name" $ns_flag &> /dev/null
+ return $?
+}
+
+wait_for_resource_deletion() {
+ local resource=$1
+ local namespace=$2
+ local timeout=${3:-60}
+
+ kubectl wait --for=delete "$resource" -n "$namespace" --timeout="${timeout}s" 2>/dev/null || true
+}
+
+# ==============================================================================
+# Cross-Platform sed Wrapper
+# ==============================================================================
+
+sed_inplace() {
+ local file=$1
+ shift
+
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "$@" "$file"
+ else
+ sed -i "$@" "$file"
+ fi
+}
+
+# ==============================================================================
+# Help Display
+# ==============================================================================
+
+show_help_from_header() {
+ local script=$1
+ head -50 "$script" | grep "^#" | sed 's/^# \?//'
+}
diff --git a/lib/k8s-utils.sh b/lib/k8s-utils.sh
new file mode 100644
index 00000000..1ef9b781
--- /dev/null
+++ b/lib/k8s-utils.sh
@@ -0,0 +1,296 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio Kubernetes Utilities
+# ==============================================================================
+# Kubernetes-specific helper functions
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+# ==============================================================================
+
+# Source common library if not already loaded
+if [ -z "$GREEN" ]; then
+ source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
+fi
+
+# ==============================================================================
+# Configuration
+# ==============================================================================
+
+OPERATOR_NAMESPACE="${OPERATOR_NAMESPACE:-geostudio-operators-system}"
+OPERATOR_READY_TIMEOUT="${OPERATOR_READY_TIMEOUT:-120}"
+RESOURCE_DELETE_TIMEOUT="${RESOURCE_DELETE_TIMEOUT:-60}"
+POD_READY_TIMEOUT="${POD_READY_TIMEOUT:-600}"
+
+# ==============================================================================
+# Cluster Detection
+# ==============================================================================
+
+detect_cluster_type() {
+ # Check current kubectl context for kind
+ local current_context=$(kubectl config current-context 2>/dev/null || echo "")
+ if [[ "$current_context" == kind-* ]]; then
+ echo "kind"
+ return 0
+ fi
+
+ # Check for kind cluster via cluster-info
+ if kubectl cluster-info 2>/dev/null | grep -q "kind"; then
+ echo "kind"
+ return 0
+ fi
+
+ # Check for OpenShift (look for OpenShift API server or oc command)
+ if kubectl get clusterversion 2>/dev/null | grep -q "OpenShift" || \
+ kubectl api-resources 2>/dev/null | grep -q "route.openshift.io"; then
+ echo "openshift"
+ return 0
+ fi
+
+ # Check if nodes have lima labels (for Lima/k3s)
+ if kubectl get nodes -o json 2>/dev/null | grep -q "lima"; then
+ echo "lima"
+ return 0
+ fi
+
+ # Check if limactl is available and lima VM 'studio' is running
+ if command -v limactl &> /dev/null && limactl list 2>/dev/null | grep -q "studio.*Running"; then
+ echo "lima"
+ return 0
+ fi
+
+ # Check for nvkind (NVIDIA GPU-enabled kind)
+ # nvkind typically has nvidia.com resources or GPU-related labels
+ if kubectl get nodes -o json 2>/dev/null | grep -q "nvidia.com/gpu"; then
+ # If it has kind in context but also has NVIDIA GPUs, it's nvkind
+ if [[ "$current_context" == *kind* ]] || kubectl cluster-info 2>/dev/null | grep -q "kind"; then
+ echo "nvkind"
+ return 0
+ fi
+ fi
+
+ # Default to k8s for any other cluster
+ echo "k8s"
+ return 0
+}
+
+get_cluster_type() {
+ local cluster_type=${CLUSTER_TYPE:-$(detect_cluster_type)}
+ echo "$cluster_type"
+}
+
+get_csi_driver_type() {
+ # Only IBM Object CSI driver is supported
+ echo "ibm-object-csi"
+}
+
+# ==============================================================================
+# Operator Checks
+# ==============================================================================
+
+operator_is_installed() {
+ kubectl get crd geostudios.geostudio.geostudio.ibm.com &> /dev/null
+ return $?
+}
+
+operator_is_running() {
+ local namespace=${1:-$OPERATOR_NAMESPACE}
+
+ if ! kubectl get deployment operators-controller-manager -n "$namespace" &> /dev/null; then
+ return 1
+ fi
+
+ local ready=$(kubectl get deployment operators-controller-manager -n "$namespace" \
+ -o jsonpath='{.status.availableReplicas}' 2>/dev/null || echo "0")
+
+ [ "$ready" != "0" ]
+ return $?
+}
+
+wait_for_operator_ready() {
+ local namespace=${1:-$OPERATOR_NAMESPACE}
+ local timeout=${2:-$OPERATOR_READY_TIMEOUT}
+
+ log_info "Waiting for operator to be ready (timeout: ${timeout}s)..."
+
+ kubectl wait --for=condition=available deployment/operators-controller-manager \
+ -n "$namespace" \
+ --timeout="${timeout}s" 2>/dev/null
+
+ return $?
+}
+
+# ==============================================================================
+# Resource Management
+# ==============================================================================
+
+get_geostudio_instances() {
+ local namespace=${1:-""}
+ local ns_flag=""
+ [[ -n "$namespace" ]] && ns_flag="-n $namespace" || ns_flag="--all-namespaces"
+
+ kubectl get geostudios $ns_flag -o jsonpath='{range .items[*]}{.metadata.namespace}{"/"}{.metadata.name}{"\n"}{end}' 2>/dev/null
+}
+
+delete_geostudio_instance() {
+ local name=$1
+ local namespace=$2
+
+ log_info "Deleting GeoStudio instance '$name' in namespace '$namespace'..."
+ kubectl delete geostudio "$name" -n "$namespace" --timeout=30s 2>/dev/null || true
+}
+
+# ==============================================================================
+# Image Verification
+# ==============================================================================
+
+verify_local_image() {
+ local image=$1
+ local cluster_type=$(get_cluster_type)
+
+ case "$cluster_type" in
+ lima)
+ verify_lima_image "$image"
+ ;;
+ kind)
+ verify_kind_image "$image"
+ ;;
+ k8s)
+ # For k8s, we assume images are pulled from registry or locally available
+ log_info "Skipping image verification for k8s cluster (assuming registry or local availability)"
+ return 0
+ ;;
+ *)
+ log_warning "Unknown cluster type: $cluster_type, skipping image verification"
+ return 0
+ ;;
+ esac
+}
+
+verify_lima_image() {
+ local image=$1
+
+ if ! command -v limactl &> /dev/null; then
+ log_warning "limactl not found, skipping Lima image verification"
+ return 0
+ fi
+
+ log_info "Verifying local image exists in Lima containerd..."
+
+ # Extract image name and tag
+ local image_name=$(echo "$image" | cut -d':' -f1)
+ local image_tag=$(echo "$image" | cut -d':' -f2)
+
+ # Check for the image in Lima containerd
+ # The image might be stored with different prefixes (docker.io/library/, etc.)
+ if limactl shell studio sudo ctr -n k8s.io images ls | grep -E "(^|/)${image_name}:${image_tag}\s"; then
+ log_success "Local image '$image' found in Lima"
+ return 0
+ else
+ log_error "Local image '$image' not found in Lima"
+ log_error "Please run: ./geostudio build --local"
+ return 1
+ fi
+}
+
+verify_kind_image() {
+ local image=$1
+ local kind_cluster_name=${KIND_CLUSTER_NAME:-kind}
+
+ if ! command -v kind &> /dev/null; then
+ log_warning "kind not found, skipping image verification"
+ return 0
+ fi
+
+ log_info "Verifying local image exists in kind cluster..."
+
+ # Get the actual cluster name from kubectl context if not set
+ if [ -z "${KIND_CLUSTER_NAME:-}" ]; then
+ local current_context=$(kubectl config current-context 2>/dev/null || echo "")
+ if [[ "$current_context" == kind-* ]]; then
+ kind_cluster_name="${current_context#kind-}"
+ else
+ # Try to get first available cluster
+ local first_cluster=$(kind get clusters 2>/dev/null | head -1)
+ if [ -n "$first_cluster" ]; then
+ kind_cluster_name="$first_cluster"
+ fi
+ fi
+ fi
+
+ # Determine container runtime (docker or podman)
+ local container_runtime="docker"
+ if command -v podman &> /dev/null && [ -n "${KIND_EXPERIMENTAL_PROVIDER:-}" ]; then
+ container_runtime="podman"
+ fi
+
+ # Extract image name and tag
+ local image_name=$(echo "$image" | cut -d':' -f1)
+ local image_tag=$(echo "$image" | cut -d':' -f2)
+
+ # Check for the image in kind cluster
+ if ${container_runtime} exec "${kind_cluster_name}-control-plane" crictl images 2>/dev/null | grep -E "${image_name}\s+${image_tag}"; then
+ log_success "Local image '$image' found in kind"
+ return 0
+ else
+ log_error "Local image '$image' not found in kind"
+ log_error "Please run: ./geostudio build --local"
+ return 1
+ fi
+}
+
+# ==============================================================================
+# CSI Driver Cleanup
+# ==============================================================================
+
+cleanup_csi_driver() {
+ local skip_csi=${1:-false}
+
+ if [ "$skip_csi" = true ]; then
+ log_info "Skipping CSI driver cleanup (--skip-csi-cleanup flag)"
+ return 0
+ fi
+
+ log_step "Cleaning up IBM Object S3 CSI Driver"
+
+ # Check if CSI driver is installed
+ if ! kubectl get deployment cos-s3-csi-controller -n kube-system &> /dev/null; then
+ log_info "CSI driver not installed, skipping cleanup"
+ echo ""
+ return 0
+ fi
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would clean up CSI driver components"
+ return 0
+ fi
+
+ # Delete deployments and daemonsets
+ log_info "Deleting CSI controller and driver..."
+ kubectl delete deployment cos-s3-csi-controller -n kube-system --timeout=30s 2>/dev/null || true
+ kubectl delete daemonset cos-s3-csi-driver -n kube-system --timeout=30s 2>/dev/null || true
+
+ # Delete service accounts
+ log_info "Deleting CSI service accounts..."
+ kubectl delete serviceaccount cos-s3-csi-controller cos-s3-csi-driver -n kube-system 2>/dev/null || true
+
+ # Delete cluster roles and bindings
+ log_info "Deleting CSI cluster roles..."
+ kubectl delete clusterrole cos-s3-csi-controller-role cos-s3-csi-driver-role 2>/dev/null || true
+ kubectl delete clusterrolebinding cos-s3-csi-controller-rolebind cos-s3-csi-driver-rolebind 2>/dev/null || true
+
+ # Delete CSI driver
+ log_info "Deleting CSI driver..."
+ kubectl delete csidriver cos.s3.csi.ibm.io 2>/dev/null || true
+
+ # Delete storage classes
+ log_info "Deleting storage classes..."
+ kubectl delete storageclass cos-s3-csi-sc cos-s3-csi-s3fs-sc 2>/dev/null || true
+
+ # Delete MinIO CA cert
+ log_info "Deleting MinIO CA certificate..."
+ kubectl delete configmap minio-ca-cert -n kube-system 2>/dev/null || true
+
+ echo ""
+ log_success "CSI driver cleanup complete"
+}
diff --git a/lib/operator-commands.sh b/lib/operator-commands.sh
new file mode 100644
index 00000000..d2b58296
--- /dev/null
+++ b/lib/operator-commands.sh
@@ -0,0 +1,451 @@
+#!/bin/bash
+# ==============================================================================
+# GeoStudio Operator Commands
+# ==============================================================================
+# Operator management functions (install, uninstall, status, logs, restart)
+#
+# © Copyright IBM Corporation 2025
+# SPDX-License-Identifier: Apache-2.0
+# ==============================================================================
+
+# Source dependencies
+if [ -z "$GREEN" ]; then
+ source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
+ source "$(dirname "${BASH_SOURCE[0]}")/k8s-utils.sh"
+fi
+
+# ==============================================================================
+# Operator Command Router
+# ==============================================================================
+
+operator_command() {
+ if [ $# -eq 0 ]; then
+ show_operator_help
+ exit 0
+ fi
+
+ local subcommand=$1
+ shift
+
+ case $subcommand in
+ install)
+ operator_install "$@"
+ ;;
+ uninstall)
+ operator_uninstall "$@"
+ ;;
+ status)
+ operator_status "$@"
+ ;;
+ logs)
+ operator_logs "$@"
+ ;;
+ restart)
+ operator_restart "$@"
+ ;;
+ --help|-h|help)
+ show_operator_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown operator subcommand: $subcommand"
+ echo ""
+ echo "Run 'geostudio operator help' for usage"
+ exit 1
+ ;;
+ esac
+}
+
+# ==============================================================================
+# Operator Install
+# ==============================================================================
+
+operator_install() {
+ local deployment_mode="prod"
+ local operator_version="latest"
+ local namespace="geostudio-operators-system"
+ local operator_image=""
+ local image_pull_policy="IfNotPresent"
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --local)
+ deployment_mode="local"
+ shift
+ ;;
+ --prod|--production)
+ deployment_mode="prod"
+ shift
+ ;;
+ --version)
+ operator_version="$2"
+ shift 2
+ ;;
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --help|-h)
+ show_operator_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ show_operator_help
+ exit 1
+ ;;
+ esac
+ done
+
+ # Validate deployment mode
+ if [ -z "$deployment_mode" ]; then
+ log_error "Deployment mode not specified. Use --local or --prod"
+ echo ""
+ show_operator_help
+ exit 1
+ fi
+
+ # Set defaults based on mode
+ if [ "$deployment_mode" = "local" ]; then
+ operator_version="${operator_version:-local}"
+ operator_image="geostudio-operator:${operator_version}"
+ image_pull_policy="Never"
+ elif [ "$deployment_mode" = "prod" ]; then
+ operator_version="${operator_version:-latest}"
+ operator_image="quay.io/geospatial-studio/geostudio-operator:${operator_version}"
+ image_pull_policy="IfNotPresent"
+ fi
+
+ # Display configuration
+ local cluster_type=$(get_cluster_type)
+ log_step "Installing GeoStudio Operator"
+ echo "Mode: ${deployment_mode}"
+ echo "Cluster Type: ${cluster_type}"
+ echo "Image: ${operator_image}"
+ echo "ImagePullPolicy: ${image_pull_policy}"
+ echo "Namespace: ${namespace}"
+ echo ""
+
+ # Check prerequisites
+ log_info "Checking prerequisites..."
+
+ require_command kubectl "Install kubectl from https://kubernetes.io/docs/tasks/tools/" || exit 1
+ require_command make || exit 1
+ require_command kustomize "Install kustomize from https://kubectl.docs.kubernetes.io/installation/kustomize/" || exit 1
+
+ # Check cluster-specific prerequisites
+ local cluster_type=$(get_cluster_type)
+ if [ "$deployment_mode" = "local" ]; then
+ case "$cluster_type" in
+ lima)
+ require_command limactl "Install Lima from https://github.com/lima-vm/lima" || exit 1
+ ;;
+ kind)
+ require_command kind "Install kind from https://kind.sigs.k8s.io/" || exit 1
+ ;;
+ k8s)
+ log_info "Native k8s cluster detected"
+ ;;
+ esac
+ fi
+
+ log_success "All prerequisites met"
+ echo ""
+
+ # Check cluster connection
+ log_info "Checking Kubernetes cluster connection..."
+ check_kubectl_connection || exit 1
+ log_success "Connected to cluster successfully"
+ echo ""
+
+ # Verify local image for local mode
+ if [ "$deployment_mode" = "local" ]; then
+ log_info "Verifying local image availability..."
+
+ if ! verify_local_image "${operator_image}"; then
+ log_error "Local image '${operator_image}' not found!"
+ echo ""
+ log_error "Please build and import the image first by running:"
+ log_error " ./geostudio build --local"
+ echo ""
+ exit 1
+ fi
+
+ log_success "Local image '${operator_image}' verified"
+ echo ""
+ fi
+
+ # Install operator
+ log_info "Installing CRDs and Operator..."
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would install operator with:"
+ echo " NAMESPACE=${namespace}"
+ echo " IMG=${operator_image}"
+ return 0
+ fi
+
+ # Change to operators directory
+ cd "$PROJECT_ROOT/operators"
+
+ make install \
+ NAMESPACE=${namespace} \
+ IMG=${operator_image}
+
+ log_info "Waiting for operator deployment to be ready..."
+ sleep 2
+
+ # Wait for operator
+ log_info "Waiting for operator to be ready..."
+
+ if kubectl wait --for=condition=available --timeout=120s \
+ deployment/operators-controller-manager -n ${namespace}; then
+ log_success "Operator is ready!"
+ else
+ log_warning "Operator did not become ready within timeout"
+ kubectl get pods -n ${namespace}
+ echo ""
+ log_info "To view operator logs, run:"
+ echo " geostudio operator logs"
+ fi
+
+ echo ""
+ log_success "Operator installed successfully"
+ echo ""
+ log_info "Next steps:"
+ echo "./geostudio app deploy"
+}
+
+# ==============================================================================
+# Operator Uninstall
+# ==============================================================================
+
+operator_uninstall() {
+ local namespace="geostudio-operators-system"
+ local keep_pvcs=false
+ local force=false
+ local skip_csi_cleanup=false
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --keep-pvcs)
+ keep_pvcs=true
+ shift
+ ;;
+ --force)
+ force=true
+ shift
+ ;;
+ --skip-csi-cleanup)
+ skip_csi_cleanup=true
+ shift
+ ;;
+ --help|-h)
+ show_operator_help
+ exit 0
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+ done
+
+ log_step "Uninstalling GeoStudio Operator"
+ echo "Namespace: $namespace"
+ echo ""
+
+ # Check for active GeoStudio instances
+ log_info "Checking for active GeoStudio instances..."
+ local instances=$(kubectl get geostudios --all-namespaces --no-headers 2>/dev/null | wc -l | tr -d ' ')
+
+ if [ "$instances" -gt 0 ]; then
+ log_error "Cannot uninstall operator - active GeoStudio instances found:"
+ echo ""
+ kubectl get geostudios --all-namespaces
+ echo ""
+
+ if [ "$force" = false ]; then
+ log_error "Apps must be deleted before uninstalling the operator."
+ echo ""
+ log_info "To delete apps, run:"
+ echo " ./geostudio app delete --namespace "
+ echo ""
+ log_info "To force uninstall anyway (not recommended):"
+ echo " ./geostudio operator uninstall --force"
+ echo ""
+ exit 1
+ else
+ log_warning "Force flag enabled - proceeding with uninstall despite active apps"
+ log_warning "This may leave orphaned resources in the cluster!"
+ echo ""
+
+ if ! confirm "Are you sure you want to force uninstall?"; then
+ log_info "Aborted"
+ exit 0
+ fi
+ fi
+ else
+ log_success "No active GeoStudio instances found"
+ fi
+
+ echo ""
+
+ if ! confirm "This will remove the operator and all CRDs. Continue?"; then
+ log_info "Aborted"
+ exit 0
+ fi
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would uninstall operator from namespace: $namespace"
+ log_info "[DRY-RUN] Would clean up CSI driver (skip_csi=$skip_csi_cleanup)"
+ return 0
+ fi
+
+ cd "$PROJECT_ROOT/operators"
+
+ log_info "Uninstalling operator..."
+ make uninstall NAMESPACE=${namespace} || true
+
+ echo ""
+ log_success "Operator uninstalled"
+ echo ""
+
+ # Clean up CSI driver
+ cleanup_csi_driver "$skip_csi_cleanup"
+
+ echo ""
+ log_success "Uninstall complete"
+}
+
+# ==============================================================================
+# Operator Status
+# ==============================================================================
+
+operator_status() {
+ local namespace="geostudio-operators-system"
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_step "GeoStudio Operator Status"
+ echo "Namespace: $namespace"
+ echo ""
+
+ # Check if operator is installed
+ if ! operator_is_installed; then
+ log_error "Operator is not installed (CRD not found)"
+ echo ""
+ log_info "To install, run:"
+ echo " geostudio operator install --local"
+ exit 1
+ fi
+
+ log_success "Operator CRDs installed"
+
+ # Check if operator is running
+ if ! kubectl get deployment operators-controller-manager -n "$namespace" &> /dev/null; then
+ log_error "Operator deployment not found in namespace: $namespace"
+ exit 1
+ fi
+
+ log_success "Operator deployment exists"
+
+ # Check readiness
+ if operator_is_running "$namespace"; then
+ log_success "Operator is running and ready"
+ else
+ log_warning "Operator is not ready"
+ fi
+
+ echo ""
+ kubectl get deployment operators-controller-manager -n "$namespace"
+ echo ""
+ kubectl get pods -n "$namespace" -l control-plane=controller-manager
+}
+
+# ==============================================================================
+# Operator Logs
+# ==============================================================================
+
+operator_logs() {
+ local namespace="geostudio-operators-system"
+ local follow=false
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ --follow|-f)
+ follow=true
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ if ! kubectl get deployment operators-controller-manager -n "$namespace" &> /dev/null; then
+ log_error "Operator deployment not found in namespace: $namespace"
+ exit 1
+ fi
+
+ if [ "$follow" = true ]; then
+ kubectl logs -n "$namespace" deployment/operators-controller-manager -f
+ else
+ kubectl logs -n "$namespace" deployment/operators-controller-manager --tail=100
+ fi
+}
+
+# ==============================================================================
+# Operator Restart
+# ==============================================================================
+
+operator_restart() {
+ local namespace="geostudio-operators-system"
+
+ # Parse arguments
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ --namespace)
+ namespace="$2"
+ shift 2
+ ;;
+ *)
+ shift
+ ;;
+ esac
+ done
+
+ log_info "Restarting operator in namespace: $namespace"
+
+ if [ "$DRY_RUN" = true ]; then
+ log_info "[DRY-RUN] Would restart operator deployment"
+ return 0
+ fi
+
+ kubectl rollout restart deployment/operators-controller-manager -n "$namespace"
+ kubectl rollout status deployment/operators-controller-manager -n "$namespace"
+
+ log_success "Operator restarted successfully"
+}
diff --git a/operators/Makefile b/operators/Makefile
new file mode 100644
index 00000000..74362eeb
--- /dev/null
+++ b/operators/Makefile
@@ -0,0 +1,247 @@
+# VERSION defines the project version for the bundle.
+# Update this value when you upgrade the version of your project.
+# To re-generate a bundle for another specific version without changing the standard setup, you can:
+# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
+# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
+VERSION ?= 0.0.1
+
+# CHART_VERSION defines the Helm chart version to pull from the OCI registry
+# Override this when building with a different chart version (e.g make docker-build CHART_VERSION=0.1.5)
+CHART_VERSION ?= 0.1.4
+
+# CHANNELS define the bundle channels used in the bundle.
+# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
+# To re-generate a bundle for other specific channels without changing the standard setup, you can:
+# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable)
+# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable")
+ifneq ($(origin CHANNELS), undefined)
+BUNDLE_CHANNELS := --channels=$(CHANNELS)
+endif
+
+# DEFAULT_CHANNEL defines the default channel used in the bundle.
+# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable")
+# To re-generate a bundle for any other default channel without changing the default setup, you can:
+# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable)
+# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable")
+ifneq ($(origin DEFAULT_CHANNEL), undefined)
+BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL)
+endif
+BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
+
+# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images.
+# This variable is used to construct full image tags for bundle and catalog images.
+#
+# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
+# quay.io/geospatial-studio/geostudio-operator-bundle:$VERSION and quay.io/geospatial-studio/geostudio-operator-catalog:$VERSION.
+IMAGE_TAG_BASE ?= quay.io/geospatial-studio/geostudio-operator
+
+# BUNDLE_IMG defines the image:tag used for the bundle.
+# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:)
+BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:v$(VERSION)
+
+# BUNDLE_GEN_FLAGS are the flags passed to the operator-sdk generate bundle command
+BUNDLE_GEN_FLAGS ?= -q --overwrite --version $(VERSION) $(BUNDLE_METADATA_OPTS)
+
+# USE_IMAGE_DIGESTS defines if images are resolved via tags or digests
+# You can enable this value if you would like to use SHA Based Digests
+# To enable set flag to true
+USE_IMAGE_DIGESTS ?= false
+ifeq ($(USE_IMAGE_DIGESTS), true)
+ BUNDLE_GEN_FLAGS += --use-image-digests
+endif
+
+# Set the Operator SDK version to use. By default, what is installed on the system is used.
+# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
+OPERATOR_SDK_VERSION ?= v1.42.0
+
+# Container tool to use for building and pushing images
+CONTAINER_TOOL ?= docker
+
+# Image URL to use all building/pushing image targets
+# Examples:
+# Local development: make install IMG=geostudio-operator:local
+# Production: make install IMG=quay.io/geospatial-studio/geostudio-operator:v0.0.1
+# Latest from quay: make install IMG=quay.io/geospatial-studio/geostudio-operator:latest
+IMG ?= quay.io/geospatial-studio/geostudio-operator:v$(VERSION)
+
+# NAMESPACE defines the namespace for operator deployment
+# Override this for custom namespace isolation (e.g make deploy NAMESPACE=geostudio-operator-system)
+NAMESPACE ?= geostudio-operators-system
+
+.PHONY: all
+all: docker-build
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk commands is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Build
+
+.PHONY: run
+run: helm-operator ## Run against the configured Kubernetes cluster in ~/.kube/config
+ $(HELM_OPERATOR) run
+
+.PHONY: docker-build
+docker-build: ## Build docker image with the manager.
+ @echo "Building operator image (pulling chart v${CHART_VERSION} from OCI registry)..."
+ $(CONTAINER_TOOL) build \
+ --load \
+ --build-arg CHART_VERSION=${CHART_VERSION} \
+ -t ${IMG} .
+
+.PHONY: docker-push
+docker-push: ## Push docker image with the manager.
+ $(CONTAINER_TOOL) push ${IMG}
+
+# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple
+# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
+# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/
+# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/
+# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> than the export will fail)
+# To properly provided solutions that supports more than one platform you should use this option.
+PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
+.PHONY: docker-buildx
+docker-buildx: ## Build and push docker image for the manager for cross-platform support
+ - $(CONTAINER_TOOL) buildx create --name project-v3-builder
+ $(CONTAINER_TOOL) buildx use project-v3-builder
+ - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile .
+ - $(CONTAINER_TOOL) buildx rm project-v3-builder
+
+##@ Deployment
+
+.PHONY: install
+install: kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
+ $(KUSTOMIZE) build config/crd | kubectl apply -f -
+ @echo "✓ CRDs installed (cluster-scoped)"
+
+ @echo "Deploying operator to namespace: $(NAMESPACE)"
+ cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+ cd config/manager && $(KUSTOMIZE) edit set namespace $(NAMESPACE)
+ cd config/default && $(KUSTOMIZE) edit set namespace $(NAMESPACE)
+ $(KUSTOMIZE) build config/default | kubectl apply -f -
+ @echo "✓ Operator deployed to namespace: $(NAMESPACE)"
+
+.PHONY: uninstall
+uninstall: kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config.
+ $(KUSTOMIZE) build config/default | kubectl delete -f -
+ $(KUSTOMIZE) build config/crd | kubectl delete -f -
+
+OS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
+ARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
+
+.PHONY: kustomize
+KUSTOMIZE = $(shell pwd)/bin/kustomize
+kustomize: ## Download kustomize locally if necessary.
+ifeq (,$(wildcard $(KUSTOMIZE)))
+ifeq (,$(shell which kustomize 2>/dev/null))
+ @{ \
+ set -e ;\
+ mkdir -p $(dir $(KUSTOMIZE)) ;\
+ curl -sSLo - https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v5.6.0/kustomize_v5.6.0_$(OS)_$(ARCH).tar.gz | \
+ tar xzf - -C bin/ ;\
+ }
+else
+KUSTOMIZE = $(shell which kustomize)
+endif
+endif
+
+.PHONY: helm-operator
+HELM_OPERATOR = $(shell pwd)/bin/helm-operator
+helm-operator: ## Download helm-operator locally if necessary, preferring the $(pwd)/bin path over global if both exist.
+ifeq (,$(wildcard $(HELM_OPERATOR)))
+ifeq (,$(shell which helm-operator 2>/dev/null))
+ @{ \
+ set -e ;\
+ mkdir -p $(dir $(HELM_OPERATOR)) ;\
+ curl -sSLo $(HELM_OPERATOR) https://github.com/operator-framework/operator-sdk/releases/download/v1.42.0/helm-operator_$(OS)_$(ARCH) ;\
+ chmod +x $(HELM_OPERATOR) ;\
+ }
+else
+HELM_OPERATOR = $(shell which helm-operator)
+endif
+endif
+
+.PHONY: operator-sdk
+OPERATOR_SDK ?= $(LOCALBIN)/operator-sdk
+operator-sdk: ## Download operator-sdk locally if necessary.
+ifeq (,$(wildcard $(OPERATOR_SDK)))
+ifeq (, $(shell which operator-sdk 2>/dev/null))
+ @{ \
+ set -e ;\
+ mkdir -p $(dir $(OPERATOR_SDK)) ;\
+ curl -sSLo $(OPERATOR_SDK) https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$(OS)_$(ARCH) ;\
+ chmod +x $(OPERATOR_SDK) ;\
+ }
+else
+OPERATOR_SDK = $(shell which operator-sdk)
+endif
+endif
+
+
+.PHONY: bundle
+bundle: kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files.
+ $(OPERATOR_SDK) generate kustomize manifests -q
+ cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)
+ $(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle $(BUNDLE_GEN_FLAGS)
+ $(OPERATOR_SDK) bundle validate ./bundle
+
+.PHONY: bundle-build
+bundle-build: ## Build the bundle image.
+ $(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
+
+.PHONY: bundle-push
+bundle-push: ## Push the bundle image.
+ $(MAKE) docker-push IMG=$(BUNDLE_IMG)
+
+.PHONY: opm
+OPM = $(LOCALBIN)/opm
+opm: ## Download opm locally if necessary.
+ifeq (,$(wildcard $(OPM)))
+ifeq (,$(shell which opm 2>/dev/null))
+ @{ \
+ set -e ;\
+ mkdir -p $(dir $(OPM)) ;\
+ curl -sSLo $(OPM) https://github.com/operator-framework/operator-registry/releases/download/v1.55.0/$(OS)-$(ARCH)-opm ;\
+ chmod +x $(OPM) ;\
+ }
+else
+OPM = $(shell which opm)
+endif
+endif
+
+# A comma-separated list of bundle images (e.g. make catalog-build BUNDLE_IMGS=example.com/operator-bundle:v0.1.0,example.com/operator-bundle:v0.2.0).
+# These images MUST exist in a registry and be pull-able.
+BUNDLE_IMGS ?= $(BUNDLE_IMG)
+
+# The image tag given to the resulting catalog image (e.g. make catalog-build CATALOG_IMG=example.com/operator-catalog:v0.2.0).
+CATALOG_IMG ?= $(IMAGE_TAG_BASE)-catalog:v$(VERSION)
+
+# Set CATALOG_BASE_IMG to an existing catalog image tag to add $BUNDLE_IMGS to that image.
+ifneq ($(origin CATALOG_BASE_IMG), undefined)
+FROM_INDEX_OPT := --from-index $(CATALOG_BASE_IMG)
+endif
+
+# Build a catalog image by adding bundle images to an empty catalog using the operator package manager tool, 'opm'.
+# This recipe invokes 'opm' in 'semver' bundle add mode. For more information on add modes, see:
+# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
+.PHONY: catalog-build
+catalog-build: opm ## Build a catalog image.
+ $(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
+
+# Push the catalog image.
+.PHONY: catalog-push
+catalog-push: ## Push a catalog image.
+ $(MAKE) docker-push IMG=$(CATALOG_IMG)
diff --git a/operators/PROJECT b/operators/PROJECT
new file mode 100644
index 00000000..4b866fb6
--- /dev/null
+++ b/operators/PROJECT
@@ -0,0 +1,20 @@
+# Code generated by tool. DO NOT EDIT.
+# This file is used to track the info used to scaffold your project
+# and allow the plugins properly work.
+# More info: https://book.kubebuilder.io/reference/project-config.html
+domain: geostudio.ibm.com
+layout:
+- helm.sdk.operatorframework.io/v1
+plugins:
+ manifests.sdk.operatorframework.io/v2: {}
+ scorecard.sdk.operatorframework.io/v2: {}
+projectName: operators
+resources:
+- api:
+ crdVersion: v1
+ namespaced: true
+ domain: geostudio.ibm.com
+ group: geostudio
+ kind: GEOStudio
+ version: v1alpha1
+version: "3"
diff --git a/operators/README.md b/operators/README.md
new file mode 100644
index 00000000..b9019afb
--- /dev/null
+++ b/operators/README.md
@@ -0,0 +1,314 @@
+# GeoStudio Operator
+
+The GeoStudio Operator is a Kubernetes operator built using the [Operator Framework](https://operatorframework.io/) with Helm. It automates the deployment, configuration, and lifecycle management of GeoStudio on Kubernetes and OpenShift clusters.
+
+## Directory Structure
+
+```
+geospatial-studio/
+├── geostudio # 🆕 Unified CLI at project root
+├── lib/ # Shared CLI libraries
+│ ├── common.sh # Logging, colors, utilities
+│ ├── k8s-utils.sh # Kubernetes helpers
+│ ├── operator-commands.sh # Operator management
+│ ├── app-commands.sh # Application management
+│ └── build-commands.sh # Build functionality
+├── operators/ # Operator configuration
+│ ├── config/ # CRDs, RBAC, manifests
+│ │ ├── crd/ # Custom Resource Definitions
+│ │ ├── rbac/ # RBAC roles and bindings
+│ │ ├── manager/ # Operator deployment manifests
+│ │ └── default/ # Kustomize overlay
+│ ├── examples/ # Example GeoStudio CRs
+│ │ └── geostudio-operator-template.yaml
+│ ├── watches.yaml # Operator watch configuration
+│ └── Makefile # Build and deploy targets
+├── geospatial-studio/ # Helm chart
+├── Dockerfile.operator # Production operator build
+└── Dockerfile.operator.local # Local operator build
+```
+
+## Architecture
+
+### High-Level Architecture
+
+```mermaid
+graph TB
+ A[GeoStudio Operator
Helm-based] -->|watches| B[GEOStudio CR
Custom Resource]
+ A -->|reconciles via| C[Helm Chart
geospatial-studio]
+ C -->|creates| D[Kubernetes Resources
• Deployments
• Services
• PVCs
• Jobs
• ConfigMaps
• Secrets]
+
+ style A fill:#e3f2fd,stroke:#1e88e5
+ style B fill:#e8f5e9,stroke:#43a047
+ style C fill:#fff3e0,stroke:#f57c00
+ style D fill:#f3e5f5,stroke:#8e24aa
+```
+
+**Component Description:**
+
+1. **GeoStudio Operator**: Watches for `GEOStudio` custom resources and reconciles the desired state
+2. **GEOStudio CR**: User-defined configuration declaring the desired GeoStudio deployment
+3. **Helm Chart**: Contains all Kubernetes manifests and templates for GeoStudio components
+4. **Kubernetes Resources**: The actual deployed resources (pods, services, volumes, etc.)
+
+### Installation Flow
+
+```mermaid
+graph LR
+ A[1. Install CRDs] --> B[2. Deploy Operator]
+ B --> C[3. Apply GEOStudio CR]
+ C --> D[4. Operator Reconciles]
+ D --> E[5. Infrastructure Setup]
+ E --> F[6. GeoStudio Apps Deployed]
+
+ style A fill:#e3f2fd,stroke:#1e88e5
+ style B fill:#e3f2fd,stroke:#1e88e5
+ style C fill:#e3f2fd,stroke:#1e88e5
+ style D fill:#e8f5e9,stroke:#43a047
+ style E fill:#e8f5e9,stroke:#43a047
+ style F fill:#fff3e0,stroke:#f57c00
+```
+
+### Helm Hook Execution Order
+
+The GeoStudio Helm chart uses hooks to ensure components are deployed in the correct order:
+
+```
+Hook Weight Component Purpose
+═══════════ ════════════════════════ ══════════════════════════════════
+ -100 PostgreSQL Installer Deploy PostgreSQL database
+ -90 PostgreSQL DB Creator Create required databases
+ -80 Keycloak/MinIO Installer Deploy auth and object storage
+ -75 CSI Driver Installer Install S3 CSI driver (if enabled)
+ -70 Keycloak Configurator Configure realms, clients, users
+ -70 MinIO Bucket Creator Create S3 buckets
+ -60 GeoServer PVC Create GeoServer storage
+ -55 GeoServer Installer Deploy GeoServer
+ -50 GeoServer Configurator Configure workspaces, WMS
+ 0 Main Application Deploy Gateway, UI, MLflow, Pipelines
+```
+
+## Quick Start - Local Development
+
+
+
+### Step 1: Set up Kubeconfig
+
+```bash
+# Point kubectl to your Lima cluster
+export KUBECONFIG="$HOME/.lima/studio/copied-from-guest/kubeconfig.yaml"
+
+# Verify connection
+kubectl cluster-info
+```
+
+### Step 2: Install Operator
+
+Install the operator using the helm chart from quay.io:
+
+```bash
+./geostudio operator install
+```
+
+### Step 3: Deploy Application
+
+Deploy a GEOStudio application instance:
+
+```bash
+./geostudio app deploy
+```
+
+### Step 4: Verify Deployment
+
+Monitor the deployment progress:
+
+```bash
+# Check operator status
+./geostudio operator status
+
+# Check application status
+./geostudio app status
+
+# Watch application pods
+kubectl get pods -n default -w
+
+# View operator logs
+./geostudio operator logs --follow
+```
+
+### Step 5: Access the Application
+
+Once deployed, port-forward to access services:
+
+```bash
+# Auth Server
+kubectl port-forward svc/keycloak 8080:8080 -n default
+
+# UI
+kubectl port-forward svc/geofm-ui 4180:4180 -n default
+
+# API Gateway
+kubectl port-forward svc/geofm-gateway 4181:4181 -n default
+
+# MLflow
+kubectl port-forward svc/geofm-mlflow 5000:5000 -n default
+```
+
+Access in your browser:
+ **UI:** http://localhost:4180
+ **API:** http://localhost:4181
+ **MLflow:** http://localhost:5000
+
+## CLI Reference
+
+### Build Commands
+
+```bash
+# Build for local development
+./geostudio build --local
+
+# Build for production
+./geostudio build --prod --version v1.0.0
+```
+
+### Operator Commands
+
+```bash
+# Install operator (local development)
+./geostudio operator install --local
+
+# Install operator (production)
+./geostudio operator install --prod --version v0.1.0
+
+# Check operator status
+./geostudio operator status
+
+# View operator logs
+./geostudio operator logs --follow
+
+# Restart operator
+./geostudio operator restart
+
+# Uninstall operator
+./geostudio operator uninstall
+```
+
+### Application Commands
+
+```bash
+# Deploy application (default: lima/default)
+./geostudio app deploy
+
+# Deploy to different environment/namespace
+./geostudio app deploy --env production --namespace prod
+
+# Generate manifest without deploying
+./geostudio app deploy --dry-run
+
+# List all deployed instances
+./geostudio app list
+
+# Check application status
+./geostudio app status --namespace prod
+
+# View application logs
+./geostudio app logs --component gateway --follow
+
+# Restart application
+./geostudio app restart --namespace prod
+
+# Delete application
+./geostudio app delete --namespace staging
+```
+
+## Complete Workflows
+
+### Local Development Setup
+
+Sample Custom Resource Spec: [GEOStudio custom resource](examples/geostudio-operator-template.yaml)
+
+After making changes to the Helm chart or operator:
+
+```bash
+# 1. Rebuild operator
+./geostudio build --local
+
+# 2. [OPTIONAL: If you have installed it previously] Delete current deployment
+./geostudio app delete
+
+# 3. [OPTIONAL: If you have installed it previously] Uninstall operator
+./geostudio operator uninstall
+
+# 4. Reinstall operator
+./geostudio operator install --local
+
+# 5. Deploy fresh instance
+./geostudio app deploy
+
+# 6. Check status
+./geostudio operator status
+./geostudio app status
+```
+
+### Production Deployment
+
+For production deployments, build and push the image, then install:
+
+```bash
+# 1. Build and push to registry
+./geostudio build --prod --version v1.0.0
+
+# 2. Install operator from registry
+./geostudio operator install --prod --version v1.0.0
+
+# 3. Deploy application
+./geostudio app deploy --env production --namespace prod
+```
+
+### Clean Up
+
+```bash
+# Delete application only
+./geostudio app delete --namespace default
+
+# Complete cleanup (apps + operator + CSI driver)
+./geostudio app delete
+./geostudio operator uninstall
+```
+
+./geostudio app delete --namespace default
+
+#### Delete everything (app + operator)
+
+```bash
+./geostudio app delete
+./geostudio operator uninstall
+```
+
+## After Deployment
+
+| | |
+|---|---|
+| Access the Studio UI | [https://localhost:4180](https://localhost:4180) |
+| Access the Studio API | [https://localhost:4181](https://localhost:4181) |
+| Authenticate Studio | username: `testuser` password: `testpass123` |
+| Access Geoserver | [http://localhost:3000/geoserver](http://localhost:3000/geoserver) |
+| Authenticate Geoserver | username: `admin` password: `geoserver` |
+| Access MLflow | [http://localhost:5000](http://localhost:5000) |
+| Access Keycloak | [http://localhost:8080](http://localhost:8080) |
+| Authenticate Keycloak | username: `admin` password: `admin` |
+| Access Minio | Console: [https://localhost:9001](https://localhost:9001) API: [https://localhost:9000](https://localhost:9000) |
+| Authenticate Minio | username: `minioadmin` password: `minioadmin` |
+
+If you need to restart any of the port-forwards you can use the following commands:
+
+```shell
+kubectl port-forward -n $OC_PROJECT svc/keycloak 8080:8080 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT svc/postgresql 54320:5432 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT svc/geofm-geoserver 3000:3000 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT deployment/geofm-ui 4180:4180 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT deployment/geofm-gateway 4181:4180 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT deployment/geofm-mlflow 5000:5000 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT svc/minio 9001:9001 >> studio-pf.log 2>&1 &
+kubectl port-forward -n $OC_PROJECT svc/minio 9000:9000 >> studio-pf.log 2>&1 &
+```
diff --git a/operators/assets/icon-base64.txt b/operators/assets/icon-base64.txt
new file mode 100644
index 00000000..01ee2c91
--- /dev/null
+++ b/operators/assets/icon-base64.txt
@@ -0,0 +1 @@
+iVBORw0KGgoAAAANSUhEUgAAAQAAAAAnCAYAAADgrJZcAAABGmlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8nAzcDBwMDAy8CemFxc4BgQ4APkMcBoVPDtGgMjiL6sCzILUx4v4EpJLU4G0n+AODu5oKiEgYExA8hWLi8pALF7gGyRpGwwewGIXQR0IJC9BcROh7BPgNVA2HfAakKCnIHsD0A2XxKYzQSyiy8dwhYAsaH2goCgY0p+UqoCyPcahpaWFpok+oEgKEmtKAHRzvkFlUWZ6RklCo7AkEpV8MxL1tNRMDIwMmVgAIU7RPXnQHB4MoqdQYghAEJsjgQDg/9SBgaWPwgxk14GhgU6DAz8UxFiaoYMDAL6DAz75iSXFpVBjWFkMmZgIMQHAL7NSicB6KPMAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAEAoAMABAAAAAEAAAAnAAAAAPyfGMIAAAGeaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjM1NDE8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NTM3PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CnxOLvUAAEAASURBVHgB7b0HeFzXeef9Yiow6L0Qhb2LpBolkeqyerFky7bWjmw5jttu4t1kHSd+HMeOd51NWSfx4zjrxPk27nIsxZIpyeq9ssikKIq9giAAorfBDAYzg/39z52LRpCmS2Ln+XLJwb1z7unn7e97zuTd99Tmk3mWZ/qvK0/3gHvMJfFl8t2sfC6zCqmcnymXXd8ns+feqd5cvoCSXDKt5167B55P6U9ewMua65d655XJlc2Vn94Hl2FiwrXhV69yZqSd6co79f3M8tMKT77IetVOfp+Wh/amdXvqxRzteC9Pbd+lT9Z9mvfTxzWjwan8k1VM9YKnqfczkt2XM73T1PL+lEqZi9Nck+t8mvdestfmjLy5dZy72Nx9BGQskxy3wdYuhkifchUGApqcCb7SexXlTyDoDULfMxmXaAHeBwDSCRJVVKkqo+dsRj2Z8MrrkUvprs6sX59XNhzKs3SavOSZ8N95zVlRccgSo2lLj09YKBiw8XTG8gsCNjbmtZlVOdqP5Adoc8KiBWbRwqCNDeYapNYgfUyNZy2VzlqEtuiFZWlH49A3tavLjUN3PhWV+TaeytjwcNryLvnA5ybyNCkaAJU5JPKfNVcatF67u154+TRB7p27kYcBMGNuMlWfqzPXvLLqmqseVy8L4PLk2vDaUoFcG+pXrg+uTddPvVY5753fL78+16DaVxXuy+RUeK8m072vbmq8jCTMzEtzVEKaS869c3n9fNPq0KPeCZpydw3Df9ZrdwW8BfI6R15XVa7M9LqV5BMLl67SXv7Jfnk1eun8nczv0qcQcirdNeaVytXtukq9ronp7Xi5SM/1zeuoS51K01e/ztl15CpQ3ZP1TuX32vPLTk8/NU2dc2Nwr3Lv6b+rd0Ya9QBTya5+O/Dd5yxiRQ6ZR8eGLJlIOwQOhgOWTmUtFPJgNg0CCXayWd0pLnjWqEGmIIiVyXjvQxGzSFTLKwQ3EFcILgJjVhgLu/rD4aCjOSpbWxKyzsFxMkJcRACoNgqSNy0LWmlZxA7vHrWSyqANnwzY4FDKSspC1t+d1kRYYnjcCktDVlGfZ+FontUuCFpymHG1ha2A+tRmFsTojicAzzzLH6Mf3Pu4j/tERBXRzwxEawJCmEf+BUuKramh2J59ps1yNMObTMGCEM2baN1yX1SJEqncXRo834X0gVCQV0xQetwyoyOWSSUsm4ay8C4EyYoUxiwSi1kgEqZ8lv/0RDPsV6cqveZzdXPLNaMEAbm++h9lzhNV1j+6p6r0R4vn1ZvLqdv0emd/d6+nkMOrN9eQXzCHxLpNXtOe3Vy5OSGR/+67Mua64JVR3/Qyl67EXB1Kmrym9W9GusuQKzDjWWnTCqlSP8lvwOU/tTaXPO2Pn8O/e8Rr8tu0nGf36Hrr/syc35mlXYZcl71n733uWaB3ust1TX+UN9fPWd11MiPIXBgptZbq9SB6xA62breTRzosHIgCn4LZjI2lxrxWJgTHqjLX8GR9uQfeBwIhOHAa5CUbQ8ukhNDUQzsFhRFHUCLRIPAPRwcvMmBd32gGYkIdVBsKO5JpC1cErXlt0BJDGcdXahtDNgExaj+ONJCYsHC+WTKecZy/pMJs+fqotb2VtpLioKX7gjY0Ombli0KW6s+zEtpOQMQMmtFcGraunpS1JTIWyxe+mZMqhPQOJ4HDAASs7eiIjQ6nLKI+TSKNxulhkzch/sQq2f/kHgJC/CAdiA9ad9sR6z9+yAZPHLPkyJBlxlNeeRoXcYgWFltBeYVVNi+06uUrrGxeswXCYagwRMKvV/fcF4fHuXSvotl/yejyTi/t5Tk1ZSrdPSmDrhyMeY16SUrz+5BL8W6UccWExLqUSY9899Jd6r/yH9dSrg09Tw7gLNr1855F1l9Slql5md7v3PzOamMq78wXXslT+z6rRgrl1mFmcffNLRPAn59faGFgbiIdsFhBkc1vWeg4/eDgkLW2HpsqOb25ac8eA0AUpw6PtfOS/xlepBGlHT7QzhhMsDAasXEQXyJ2aZWYFJy9N2MTSBDBUMjGkUAG+yesvz1oAyeQBiIRe/OltI30jVntvJCVlEStp2vcJgoDVlaeZ6UVYTu4FSIC4o6NmpU1mA1BBDrbeUYqiNCHBCpAWUXI9ndPWBL+GoKtS2qRVJBBOoHtuj5moFoiRlIP+iAU4q4SJM7+AviDTEJioMdO7NxsHbu3W2p0mDnJWu2KNdZ03iUWLS5xecSRh3s6bfejD9D5iLX+5FXb//wTVt483xZtvNLqV6+BQLAonkJ19n34RXJOW9Sfu5pfRh0/d+P/UfBsZ8AnFMNDcduzZy/F8iyeHLEhkH7Xrl2OAEgv9kR+T4L09P0c6eGd7AX6OLUSeFbeMMwPDdt1QzmFaOEIenkihT6OSE6evLwssD1h+aVZi6ECDA+myWeWX4iOX4jaAQ6N9KetYVnYdr+StnFsFfMWRW0U4lBdF7IiSRUZuDvcPz6WZ2OgcCgAGofHraAEW8FYxqqqIvRtwoaHUD2KhPQT1lBEXyoK7a3WOD3LGgKOk6CduoKAExRBgFhEIyHLauzMyVkTgLw89BooSOu25+3oluc0nzZvzXqLlVXaW4/+wFrOv8wqFywhD5wdahhAD8ovr7S8xx60ZdfcZJULl1jv4f129LWXbNv3/smqFi211bfe4QhCFsr57+f6VVOAX3X7v04rdfq50JsAcJgYjVtX9xF09ZBlAoPo3RVwRbPBwQFbvHgxHBGxGpU1GkWx5xoaGrKCggIQKkSeQfde0oMQOxKO2nDyJEjU7wiDCEYA8TuYn28TsTyLxiKWHUvbwjVIHcUFVlgRhCgEEdfHkQQw4hWa9R4ZQbQ3O7E3a6XNoFF+nhWVF1oqELY03Pl4d9hqqostGi60TBBdfmDAYhXjjCFukeGI5cXyLVSTsngojRiftdEh+jmCYa8amwai/0Qf3H1E6koQ/T5tK88J2+E9tM/YRBBcn6lXhEz2i7MiABL3xxD39z55v/Ue228tF1xu8y+6io5V2cDxw65SyUSiKWpA1GciS5Nwd0kHe598yAqY+HBBzM59z9020t1pb2663577mz+zNXe8xxZdfuUvJAlocGd1+RmnwY2Spn396dX8zAV+epX/f86hufeX5Zc9DzJqC0FlFc/ClSWOx0fifBfHDNnx48cdggtmheDi9tLbfc6v/vjP3j2EPQuVQyowEkEAxI8uaLZgRQnIhh4vexCcu3U0z8qrllt/ugBikcEw2Ef9WOu7U3b4hR0WmBiwaH7Q9nxD9jKk6giqQ121xcoLQGaMii3UmV+O1JC1ozt3oWp3IsLnW9/+Esvug3owY8n4EMZICNwghAe1oW14wkZ74pZOMqPZAmcn0xhPtIH8qKv6J/Ff6CkDpwySUp5+KgGQHj/a3207H/qmZcaSdu6dH7bqhSuoKIMRJOUm7nQLF8ovsMZ1652BMDHQZ527d9qSK6+12uWrraJlgb31yAO2/QffseTQgK2+5XbqnG40Ol2tuXRBDYug66yBSBnnuFTLaV6dmntGRq/92ZkCcJWszK6qdXYWJQGYAsYJl2da6dl59cpP8+9+9tzY/a/unssjnVQLLb1zsvyMjD//F8FDVqzkV3UxRje/Z9EHLZVAaiyZRIw2K0anFsxo6oTo4uz+JQSfTgTGx8cdcZCEIBVAd+VR2RDWs7GRccsWVdh4tAZigBQB980LJB1K5QVi6PpyxSHZqgMgW3qUPgwnLYbrLxSNQYBoGQ49nuR7xHf1yfo/bsm+Ad6lGCceizFZ+DGwJyFefaOWX5LBeIjUUDcPKboRAjDKOveCi1JHRuhfGjyNkyfJwJEQ4uAoYr/zauTQS12S5yIIcTwjAchDjBgbHrCdP/onB0jnvefjVlRVC0Kn3KR4rj5/Cmfe5RUIQwDWvuN9zgZwcu9O2/Ktv3cLkGFyZQhc9673OgPhrk3/4hZ11S1vd8bBmTWd+i0YjjiJIUs9IUQ3qRuycv7Ui8VzlyDj571Ux4zy0FEBlUMMT5XpOdxqJbWVjB/RcGZmh/yJgWEAKG7lTbUg6lRlbj6p3+umly5X0qwGz9hzEZeBE93ODlNUVTqj/jMWnOslTYuD6hKxEuL3HG6HeNeyXl76XMX+tdIc4mJc68a3X7Wg2s3lmeZGPZSfvLi42HH8dKDf+dsdaWbeI9imhNhCdnFLrYWIwvDw8AzGpvUN815qsBAzhQ4u2l000W+xIhCpstrincdtbKiH+SqwgqoaJGb0cDwAMnFNQAh6Dx7AXpax8nkFFiyoBn6RSMJDtDNuaVx2Bi3iEUQfJe9B95xOokJEaItYgeEebAplafqPoQ+bQuWiFghS2On4o0OeyB9EUpd9IIQ2k49dIA+7QACVXHyAnnhjp40x1JRwBO7PGp6WAGjQWTq++/F/dhz8vHd91ArLq52VX0B2pkuGwsMvP42t4EWARkiB8QF3iwatSRYFRz1jQgO28oZbmKA00sCPrLiuzlrWX5IrM3cLQRDt6Kuv2t4nnrB4T48V1dRgS7jJmi+8gLpzJE5IRP9nI5/6ISA6JZ3E6Ujnt6w63ItpSOrqYPxSd1STxpAcGsYT0m715yxj4cbt9e/+yM6/6xarWjLfIY7rixqgnmAYseyNvXZ08xt2w2c/xuJDumknkxpHJfoW6lE/dUwRsw0fvRNby2LWwE9zFflddHdXv6MTuJ9wt+584AUIUIWd95+upX7PzeXWbMY4vCq8cfvV+fPjfRfy9x096ZChZnEDkuCwvfL1x+36T78bAxcKLfW5OSL7dELmSrupk8ipjp16+eVUx+zLvZPXZfor6hPRGe6K2xN/8bC9+8t3o1LKKj+79NR3Va21TjIHQeYvCGKMM7cS1YXoIyMjrn/e/HkVqb8iCiIIvvgvyWA0kSAdbk19QuSKmgIL17aYxcCJMenfiNXcJ7IhOKuMbVJ/QTBZ7+NZLPggptQRPBGBDHlTzA3EPZNSzAzoyd3IP56QrS2DPSGAfo+XgCEqZiFcALxR9+gQtoSiCJ9CpIcxKyootooltXD8QevCZgF6QBSAFYaThYGnaU8qQDoF8aBO4R36EGsqtQhpZmq6Zj5J7z+y5SkbaD9i577jI+j7NR53ngkxMwv53wDoJObJVHwY/f5aJiRMwwyc3hWUlln3gb12cu8u0vJs/sWX2qpb7rDB9hP2k+9/B0PiIiusqmQAOWT26+QuqeH469vsha98xSF+/erV1oFF9+m/+JLd8qf/w2qWL3WLlkZVGWfRC0pLqEYkWMEcYSYsiY6UBHhJZ5KVHmChE1iGBVz5xRhehGi5dHFp9TG/KMaCIrWIy0P6hfBR8uq7rr5jJ0C6x/FsLHX64TWf+oirTwRPxC5J/aH8iEUKPEOTykwigL5wCRja3zwAF4lb3erFrg9KFxDKXz0potJPEQsRDfVN/U4OxgEQuJmgRZfWKLdOei+gTlJvflHBlLTkxqj+AwjUH4ri1kVMTWORLsAopTLq+8EX3nRjrl3SAAwU281fuBuA9MYtYjaKBSrEPVIYZY48wHM+dp7TY9RVytzNEtdVPoUvezyZgqmoLdmMPATUOBNDowB41ImoWUTVAAggMTapgBcnhnuD01+vFA9zXALyKCpzw2LE+WjWhvuImMuDcAFagyBMc3OzxYhRGR0ddUjvGwO7u7udFKC+1Nc3gLisO2sZIQIonuyxijpiW+CgpU0tlqmtohMpIvjmWc8h4CxCPwf7LZWQ2xHuLd8dDcooGMW4aFjz5QIPBMNIJ2EXh1DWVGyV85c5YtB96CDv0pYaRpwP9CH+C28KQVbsBOUxVIB8R2DEgjA7WFl9vdU0NtrJw0esfd8e6p7IERyEChA/gMQgd2VAbkGQX6pAirUJ4wnALjk3AZAFcQQXXuu2Z7HuX4HIt8RxNsdF5pjo0yVFiAFYetWNEIIRgAEKio9UxsAjrzxv+5953KkG7Tt32MaPf8LOffd77Ykv/rG99fCDdvFvfoSBnEoAhFB7Hn/cidbXfvoPrWrxIhtoO24ncfOUYzgRUh3bstX2PPo4Aw4BsGV20Qfvdsh69NUt9tZDjzmkKaqutIs/dLdDyi3f/D6W2WMOwRduXG9rbr/Rtn33X2y4swtAHIbrDdjKG6+2FTdeZd37D6PG3OdwS4Rg40ffRx1R2/yN++FMPfbiV79lGz7yn+zlv78X1ed6jDolPH/fEQAB+5rbr7ElV10093QBzep/1aImu+Mv/7tDCh+8N39zE8anfFv//luQrHbYgWe32hWfuMte+OoPWGD8zO09Dnk3fvjt1nzBilz9HnEYaOuzl7724KSkseE3b4ZQzccT85QNdfbZYEePnXPrBtfeGz98CUKTZwUg+jW/dyfEdr/tfmyrI3QC4FU3XWjPfvlBu+q/vt1JMs//7Sbrb+12yHHOzRfaqpsvQLLZh4F3MwQlbL1Hu2zBJcvs4g9c5YiIOiaiuf/ZXfbGg1tc3wvKYnbN794MAcm3p7/0EO+ROlp7HAe9/g9ugyFUW9eBDnv6r37sCE9pfZmbFqbqp16amxCIKiSIIhKn4NLxHnzsIIS4vKz8kgKE+Pqu9DEYRwrblpBfaSMjw+6utRljDUMF6N9Y7uvyQ1Y6RPxLYYHFaxayPkUW7T2Jage6E2IbLsBGACdOoM9PpJEoeB8mKC6dRmdXsBHYl0Uwly1iPBW1/ArUgnGYTY+8DEjLg2JQ9DmZ52IK5DnIJzYghMsxJUkjD6IdlnyDiJ/G9hCQaoJ0A1sf6Q9YNJuwskIMnhAhD2+RLgg2EgGQ7s9Q3UcCwSmXqOzxHS+yiAX49i+nEl/8PCWrm6hTU3MpaoXrzU3ft2f+6gv29J9/zo5tBshAznwkgSs+8SmHeDvu+x5cvxoku5X3rwJUR1m0mcKJFmAcMWyos9MKKyutuLbWjm973Q4885yN9vVZ1779IGu/7bjvh3bJhz9oN/7JH7kBvvXwY5boH7TXv3e/rb/nvXbdZz8Flw7azh8+DJJ3W+vW7Xbtp3/Xrv7kf7HSefWOowye6KDshF3z+//ZLv3YB+yNf3nEhjpOOiRac/sNdsv/+kOrX7UMA+bDVtpQa+fcfq1VLWy2iz/4bvodtH4kAon2EtGazltlt3zx9+zCu2+z17//SI575+Zn1k3I0XO4zb73oc/Z937r8/bkn/2T68eK6zfa/qe3OMTf+u1HbPm1FztRv2vfMYhznd38Pz6KEfVSe/kfHiCu3FO5VLXmTMgvFeLWP/0o6tZF9sLfPeA4spB/oK3LNnzoJmtYs9Ah3FX/7Z12x5c+bsmBEdv39HaQd6X7LL5sta29fYPj2L2HZZEO2o4HXkGqGLXb/tcH7Orfvd223fs86kKX4+xC/Ms+doPd8Jl3QbC3004vZXKgxry6uf29W+1dX/4gCJO0fc++5SSQzn3tEMBau/Ov3o+OX2Ov3/eaQ/rnv/oksSNL7Z3/+702f/1CEBm16WwuiEQQhPEi4eB2SB5Cdt/YlwCeRACSSIVxdHZ9H8Dt5hMIzZ8QVGUcoiLWy4+/cGHQqqoLLJ2N27LkTluRxbKPETAULsUmELPCEmwOSEVCRiwNWPTh/rgICe6FeYzxTnmRmgoK4eoQjpiC6kaYiyFgPIU6ITUFNYB6wqWlwFglEkKTRUrqLFpab6VIyZGiUghVwHpOtNnme79nOzY9bPGOLmCv08YhWiEkBkmKCgQKI0EpwEj9URCQ0FLp2gMxE8vooiYrMQRCHXgDd99VUDZcD5gYNRmTFxU4kV4UEoqlGjVpMy/cEv29AOVfw6FarayxhU8ziPh/6XyxK1PaIEvmYuroRlQaw7V4Cbr9j+3QSy+AMB+wvGlBQn7t0tb17BCttdX2Pv4EgDhkK2+6kcVGpGJBD7/0Kp9XmNQ4QDmKynEIblxqNcuIU0CKWHTZBvpxn615x82Iu6WoFF+36iULUFcucUPQ2GpXLGXsMTwWi1E3qqjjCMhwvh18/lXb+u0fovOfyFFXqC2UXeJwfkkRCCjrLSIy81FcU2kDZcVw24foF9QY0cvT5afN5fRJY2AaQ+m8GlInrKi6HBsC/uKGKiSLd9rDn/lb2/iRd1jT+SsAlCRcJmp1Kxc6YFq4cY1tv+8pbAgDbm1kHUogng9iELzid+50hHrBJavoy5MQs16Xp2X9csrPR+xP2bxzFiLuv8FnJxGdCSdRqP4wqovUiyjqQwI1QkRKa922/RBE50LmKGo1qAdlTVXWvusYc1BAbEcVc1bqVImi6lJnqS5vqnTrJjBpXLfADjy3i7Z2s3YJG0cd0BUmfLVuWYNrr3Fdix18cQ9BZ6NIV0PEkqx2c1y/shFpASOwDxCu5Nx/BLLieGMgQmKUz8iEFRZWu8wS+2thIjIQ5mOsFeIXFqJXQ0AF6+3t7U4NUB7ZA0Q0QljXInm9Fsqg1gGb5Yj0xcBIUdsxjOV91rTuWhuIlBBok7HDO7ajsvRiBypD8sVgyb/eQ4eA8xELV1dZITaaTJL1Re+3IBtzaG8MxE1jxEtia5HaUIpUW0je4lLcjSVl6PFICLj/qoaPWXmqy7ohCG92TVgr8z46OAbhwUZBdRFCjmP0IZUA+XEVEsFMbXKD4mJ04j9GQ/CcIc1BAADevmP7HKLULFnrDHSzp1e6eBpr5f7nHrLW119ETFsKV/Ysm8orJCtrXGCN567nmUmvqIRTLbIFGy5HJyxnQTucLUDERnk14QIqRRE2n7+eOrcgLr8TQJDPM3ex4GEmvKS+Dl3rEFJCK7aDm61ifos9+rk/cYAptJJxZN66NU5q0T1WUQ5VbHULSEMO8NWmnqMs/g2f/SRRjbutfcebSChfsdv+/LPunRBQEyZAU/4w3oZXvv49J4Usu+YyV39/64nJvlMIqjSF2ELkfU+9DIF6kdiHmyw1Mkobe11+jUhjnkFUSZNaIc/ArV/8BO3KYEo0V85IKOOgVAqJ7J6LUbVwUQ/Y7fopDPPEPa/v7pnXmlsvnfzuGSghXfq1CIzUkx//yTdt4YZVNm/tQju5r9XrG3kdYdew9PEvnt3csLaTbfA86THgWes+Abdx12RZABBkfPiz9zrVoHHdfNoiplVjyF1S/dSmyrv58d/5Y1DWXLV+mdPdZUZSzDu1Ob1ZnLAfC79/nThxwj1K1BeHn35JBZChUPYAPevDCK25IWKFoXyLlGWsDANcFvXiWFfSguMjVrT1PsusutYmmhZ484KoL6OfYEGolobOaf9AJoVHATU7gJ9fSB3EHjCewd4UkldLIjqGRsR5bTqKQHBHO+MWQ+hZGB20pdEOdPgeq5RNqSZkB9sLkAhKrQqJoaeNPQAl4BuSRkFg3OLUrShFXZrPVBJDICqRbCPyFGheZkoAbnKJJIIAlNQ2gqQVLIaQxdXh/og79hzeg4j4IFRv0JZdfRt656UOAd2iAVtSGepXr0O0PBeu51m5BXiicAsvvZLBk4nGxS2nrNvqZJYy67APPInU0GY1S7GqT0oBAraALb/ueqzlf22P/88vwnmW0Ndjrl+ydlctWijIpF8jjtvvQiyqW7WCz3IMjPfb0Vc2k2eB7XnsKbwN58Ed23l+GhH+Jow0RRjhdru+C6jbtu+CgK2G2ByzeG8/lHyB7XzwMbwNa5EW5mNr2O7sA2PxUZrE6Imhb7AN9QSu7Yxh9Gr4ZA82jxJrwDtw9NXtOZvCkAPs0f4huNswRA3RjH/uPwvTte+o3fvhz+sr14Rd/jt3OeB7c9Nzdsdf/549+6XvYCd5CeJ3KYCUJrJyF4ShGMlpM20VWTHtC4mG4fICvOolTfbmj160dXdeyby+jqrliZQSD52dBQSTsS7eO0Q/FyJtVDB/cF0kCd/fP9I1gGQx6Prtj23hhpV4brZa3Yom3I49rFcfnH0hc4g9Bb3TvzxruDcaIXQGwB7pGbbGtZIIKzEWi8MPujIuL3CiS7Agy3VBaQHib5W9/oPNtv59G+zQy6h6SAU+XfDbmesug5iAXVtyZQQbBxeE7LrE6RXx5+v+vlqgd3pOQhRlPVc76rekBHkPZOEnkM7KMiVwcBC0MGTlpUQAToQhNsTzH3jWDmbGiAcKw2CAcQJzAiGs85Fy9HwiYzN4CUDCRNdhjHUDiPtsmCOaFkMBzAvpIUYEYVUJuFFoJTXgX/eQLQ912cpsHxsc86yCoKOeTKH1sGaNrGEdtGUM43eyb8Tyk0PsJsxaMXE1NfT72DgGY4ynKXAiQQyCRH6pACEIoSPsoGGwad2Vn/corRiYLM4pO7r1KTj2cvTaFY7b+JMgS3r3wV0gwjfpXAOGrnsQkde4ygTEqsfPK6SI93bBNf/GDr3wJCL5Mxj/nrNDLz7D96e5P404/TT6YStcuhKOcKmrJ4Sh8NBLz1tJXT3Au9QBgl8vKwPQNPKZ51yAowP9BBZdiahdQ39brH7NakTRRtv9yKMg3GYWJwbB2YBYiqukpQmAfRyE2QpAtSD+3+pEyq59B20vRODknv1EJd7kVIGjr21zSHd8207rgChc8N47IDYL6FO17XvyRewU2612pWcYlaGxomUeKsJR62/rQPpYAeL3QoAWMDeL7MSO3dgpXnOGyLJ5tU6CmLd2OUFRB52EUzG/wXFhLcgQNokCuLzUCc2DPo3nLscYt9vp/Q3nLMLYWUf7b4Ksi51BML84hp79KvMxiL3iDkcMQlh427bvQzJrsMWXr2MudkEgtgBwCbv0o7eDVIX0sZ95o+/N1bQDsBbl2xsPvIhX5agtvmKNM+7Vr2yhviIksgOOg1QvmWdDJwesEZtB3cpmEHmQMq/Cxdvs4nuusdpl8yBq+L9BunlrWtzaDXX0Yy9pos0Y64skhS4sg+L2f3nNEYslV6xCneohTyMENW4N3JU3SYCLYKlhTbPVr5iHyrDb9j69y70rn1eOpAiXzdkVlG/2JfjLoFoM7j0CAYATQ5PwauNGEzf2kFxILfHewT9pPqcXx5Per3SliUh4hCIPNYGxLYCLIzFk4bBGG2PEJvQQclxeEbWyslLbPoihzgpgfklHJAJhbGiBEhAbVQIDnijISDf6+lFsIyFclJEy5goJN1aMusA8hWKsHXPf12/Ljr9m59QlrbiqwE7GkRD4N0CQTxLGU1tXZmORYjs6zA7B7mF6jVo4jI0GO0Mp+4UV7z/M7qB8PDxpmEKkIEgYMs8YA3WJIORtuOfznAfAQPgv3VUhv6//85cx0lXYmlvvAXnQUxAvNBnepp4XQdAf2+Uf/2P0EgJNcGm4BaAOh6iMT9RbuuIwIb8vfOVPcfVdZqX1jY6bu8mmgCujZiE6sYoKJIZzXHl17Ik//bxD/ovu+ZAjSH69flkhhpsxiI5iDjwJBRLELOq76pZkoXwuDoGJULAQGdz3ILKVApVkTRCXlPSgOVCMgYDqmb/8qpMaVt10tRNZZTX13YBOPGXxVU4cQu3QHOOV0Qeg4J0XmORZXwVAipgUkmlLqOOqMDm5tiYo6ygx/dCl9lWZm5tcmripvqt/kqy0RkwZwJWxB37/r503oGphgzcHVKP65Td2CEAe9VFr4fdBa6M8EteZDj7kUdsQHY1RcyzDmfolbi/jneqSqqD5VTlfMhChkSShNPVJHFx9VRlPTfHa9st641Jb9Ie6XVtaF9nDKevsC8yf2nbA6eaLNgVb4l5IBCEAWJKm1wdv3vx6GY2m0V3KP4YU0/vE8yRnsIyDHkTnHd9HXW6SNfap/NOfVcF0xJ8iDEFrrA/ZdZehPrJzb4yjdOpLCm0Aq33/QMKq2dbbFWu0N+L1iBi8RyqcmCD2H+v9hNXQKtx4BA8HNrVETz9ErhsRPWbFDS0gK1uKS2qccTyL6lDTUGILdj5si5twW2oPAsbBgcGk1bLZJwn3D2AolEtwNFRk9+5BbWeskQiBAyc7LQ/7hkKNBYdD/WM8I+hrkblGOWNAZwtEIAqnRgIyk+lU0i30aF+X7frxt23t238TyoQPRSSKyyEh8zbY0WrhfvyaDM2lsUhafS1+cU09d20FLsJIVMiCR+Eq13pIRx4BlABG+fWsOkRIFEqtLZP52AKS+Gmd+sHb2Ze2HLs6KO+A1rVNddyFJF79AhipH+6/S3f1qEwufUIIzLM3Jg/g5ZstqauBI8acKKg+SIxSPZO6t9+uus4l6Uf90JMuHzgFqNorrv54qk4O4Lhl4BpUM+Py1SEvPZeXHAJOB6u8kHiaR73qT0Wz5tlNpNemMvn9zEnhKuuQTYQIpHOXy+Otp/Lr8t8pv1QCv3NOTaAuEUtX93TxHuT32qOunL6v8m78uXp9YuG14v0VsutybYHUIlK6/HnTs+ZOH10aM7gDVmrtpnuk1IiXR/lmX6p/pC9jtQTtjOfLEIheTfSfLqkApVjYxdnl9pO+P90dqLLKozS9k4FQ8fyRWNrKiwME3aBCYIwM1UDAOtJWAUEIcRZAf6TRijD6ZfEAifDlBQtRqXoJBe4Dt1Kowb10WVJFHEJLG6hEycEuYCtIGzFrWnw+rsg8a8KzEIoitlNXAilDaoe2+Q6NwKxoMg1MTtCGQovno1IM4BZMjeWzDaDGwvFWw7wAnIiQyY4lT0DY4hw4IsIQpp864CSLdDDDBuCmEy6mwS/aeBNbeJ+3XY98x9a8/R464xnkpIeLe26/7x/d4s+YdMop/v/Sj30KnacCClWG4e9KxLfH0bk3QBjqHKUTQghwHKIC1EJARxAEYzy78wKY+NOvLYW17uowlxCQGrwvP+PfSfBxbQsxxwlRvs3Vouc5q53W9mRzc6VNvpz18PN1dbISv89XfOI9rn8+t52zr7lSLI2P05P1/Do8/ExT8bPMMYMT8EvkHQGBtHmnsEiAD/Fxc4G/HI+RLsG7uLwuwZ++6y7ioHQRIEUCCnFTcPZMYZkVYvSLEvZ7vIPDNY5hf5pfZPnhAstHJAe9Qa4oSI0HJV8RiCLCbBcGI8MgrmL5qQpCy53vOr1HklgKhAxgO8CxYBMHWq00LkKBKg1zzMTT1t+PsZExxRhTAomgsrLcSvsG7ZqVDbanO2EJPAgxtgmPDbO7kHPDNLc6oGSMesNQkHzcje70ItI9NWCWG9DNL5goMbGouoEAkXvw9/4D233v5ZkIMETnmqVrQOQGh7yCKP67yRLSdh/YhXrwlObRXeLGLesvdfr/4ZeeBbF+A7FXpPw0F3U5yptAhNEmZ1V+msuJwuJ+gmxHDU6TcXYy2SVqqm6nHszVhJ+mqn/Ry03q6StR6K44v7iFu/y2/SJ+ef8+mU6Cmx+90OVJXyKqUkW0hr/IFVSkGPWP44WQyC53miz4Wp9/L5eYQlU122ubUJti49bfOWGHdzJTGgtMzN/2KwlAl4iDTwjcd6QdRQo6NYi50F4ACfNR4CeYjtlQW7+NZcIWKiFCj9N62mB+HQl0eP6NJ3GZ4u+PnxzEHcfmHtZjDN09m0YlUNBQcYVFEuwXIZAoSPxAOH8CI24luwGHrDiYskaIRj/uQUXtxTgNpFVRqcw/sUJgdcBaKvIJ9EEFoP4ohCWAO1H9SxM4pH3HE8kBd2qB1k2BUGMQhyh2AQmqKR0dxtxI8pohAQiZFPwj5EoSC9C4dqOtvuk37M2Hv8Xn21a9eLXbDFTRvJgKqESTAsAJDkUc4kRCTb8kesiyufadv0GyRLqcXMq3GXA+4wuGn3PWMBnlFDkViCWBqM1B/KYpdJ1C3HzFdbXM71Td0/sw4xnYla7ezhZLiezNF67LieYzcp35i4N/DwlESBz3PVucIJ+bL3EVqDpTYm/88En6cQ7uzSqIwFmMYY7eCUAlwvcc63A+/eLaMucNcPYJt0Yi6mdft8a1+9EtNtTeiwH0atv3/E6MlseIRHwbgC5r+NkOeI7O/hslyQBWgsGxugndGz4BLnFoB7vp8lFx6UOG+S8rK3NEQJKAEF0BQVIRRAj0UaxAPpKvgnYkBQRBxGiU0F6i7ibgqv0Y+fIwrDXjfSnjfL9dwQXoFpQfJY4igTEUxBtmv0o2ifSQDhFf0Q8AcFIQunu4oBQ842y/CuxoAQKFiiA+0QpCjZM2P33EglJNK4ocvOhQ0VJiMUqJsVDvs6x3CmLQc5QoTNLDEOn6pQ02kg2CgxzQg35v2iEoNy+DlRShE4rkEtQBomPYD2ATBC2hwsxYD3KHdXwSxybFsQFI1K+cv9xW3XAXPv9NhMzuxU98CbrnYg/wBVw04ER46fDTuI4mWcgqBGk45zyXR5uLlMabXLPcRT106Y4RQDrnqpvf7hbNATDIPnmRRwFDW775DTvChiC1F8KSu/rWW2zdu99BeRmPZFRChGOSHJLxLcACqnrpmALuzrf2ucirRZcqLNezG4hq63IqiAx61OUZEF0WV04GS41H7cpopgjBRZdfBFFUQIdHEL1K6AH51JYQXtRfBErf+1s73GagtXe8zRkfZZn3RHjXuOO26quzB2j1eBaCq3e6u/Zz+rbaEsdS8M+Tf/4tPA77lQSHybcL3ne9XfSBG7G0HyQYa5Nd+V/vZC3r3Lhc/TLC0Tdxdy8eAJsE9TtuT9r+p7ajAh4gRPtyAnLeRI3bybqsd1Z4AY/GwqDdnP7qCIK3Zm7Qp/kzjr2hgvnIJ7J0kNDZkXiPQwqtc2tr62QpjUFp/lg0r/IQxNHV9awPo0b81p4HNnQRRjwGxy1HUipD7N6bV2S941UWJcQ3TLTg+Ch78tnkU9FY4E7xlbsvCOceHx9lHRUpSGgwIcUhLP7lGBKjeBeQu60y1W6LxzsIOc63ahB2qH/E4kQZKlR6eARjMrkKamN2cFe3lRMLMB9bVYoArcLiWhubICCNeJNsAKIC400Tnl4IwZYrVLwUbAUuaQccKyIyUMbiGQRASBCCkxdV1WHkO0ohAfsEvvPVBKgsxjvwVTdJk7N2hgcZ845zDFjbDmK+efYvAZ/rCsDmLoc43qP+ahEE5EuveRsusPPpPJwyd8nCv2vTj3B/vWyLr7yC+IML8OM/Rvjv/Zw4NB9vw3q4VgfbJktAim582vVMehiE3+PFBixfgvpS5YDXSQK4+BShV7d6ubfI9ClJrMLJ3fuJVizEx72EDnmA0bX3ENSVeAC2YRbXVhFheBykeM35+VsuWudEeBkUZc0VoSkjmk/bgofau3AT1hMd1+Ci945t2Un48Zv4zJe5iL/l122E+suYCp1g3NopKAitZ0OQNhClMfSMEAknAtLf2ol7sYWgK6Izc8Y4EaKdDz7nkH8jLr7y5jrb/eNXWMNSpLg4fdyOS3A/cQu7XZxAvGeAMY/ixluA8SkOUWd7b3MN81KGP34A6egwbtYq17Yi87T+a2/fiHuuBeNouSMaCu3teOsYhCZCVOJi3Et4W+j7r9slA2yRtszCQbNw3n4iHHWun2yLivkvKcHlCsOQsU8nAkkV0HfBoC5JAcqntBgGOoXSMt0Wx6c+BDKWsSGsEgmhb2DcTtoCjuxigw+EpryiySrZsjAwMkhcSDF6PoXYuaNzCRJY/gPAlAzdQfbsFhHkf9H8xQQC4VkYPm7FB1l/du0l2MevXgyxi1BifEcHpxMBF2vWzrPDh3EXQ3jya4otjyjBAqT29ADjQpVME2iUZbdhtLLK8rrZIwLnFwGQxV+uQElG8jbFOGpcODCFmRqxCCqDVwzAwRcforO9HPmFaMpESsTXpW29jov/lPVWnoG2Y3CdxfjGL3AAIp0jjwnS/I52HHdIF2tsFl2iZj6ka3//geeedgE+Tedf6DQN1zAv5a5r3boVQC5DHH0/4lOZFdVW26bf/0N841uwN1yA2/Hv3G4/1Xfpf/mobfv2vRCzDpCmwnbc/6Bd90efpI0IMQmbnRjYc/CIVbzWaJf99m+hVuC2/PLfQ0xaQIYeAoe22qX/+R72DTxix7buYF4aiSV42q78b79FP3ZavLvXDr+41e0DGDjRSbDRJlyaZfRjLSclVSDeP447c769fu9DbD66EyJaTxzCDohTH2PcApe+hcCeb7Bh6Xb8vo3savwnjzjBmRX4c+2nP4Rffcge+aOvMofL0CuTbEZ6xG75nx9jPFB5QTJzpp11/hwpbPjaP7zbEYAj+P933P+ce/Xq//djR5R2P/oqROag/eYPvgChO2YP/dE/2nWffi/xAmvswU/9AwShwxEDcQkRcQHL3qd+AqHdakuvXsuxbh226TPfgDBhIccir7De2774fqRGhedqHX+2SyUE6P8aVxgYjMh+wQfF0UkBSW2YkaQJMsfZ/SiuKPFeabIL5BFDP/3SmBwhkFWfPONEEyUS41ajMwYwsA2NpizGbr0JiDWbA+DuSFWcOizio3JZnc7DpiAhorYKZ1ERqAanCe8gTBnFCEAciNyyiYPPUgXzyiEhCO42isBPJIFF1Tci+5bDSCIQNFqxlgWovcB4kNOKx1Ehzl1xno3T5o7AXusnBB4ss9Sxw65PRfQTxQXcxSvhiBsGQbwBI4xjJgEgk0TXiuZlIPkj7AfYaQsuJrTRie5BEGCJtb3xmrvPW3cxSD1zsqZPnJ5FBGTYKOb0Evm8GZOliR4cfGu7jbUfF4klhJIjjs8518IE66hz2l0X4xThydOFc5Wq39pckySUs4C8ol5p4uEjHDOm51GCJsSF0hColTffgHRwHpx/N0Eq++22P/sCrkyOTdq+E2DA6Ib4W714AYj8McfJH/38n4FECaSLxwg8WWnrP3AXxG/QfvTJzxMJeBSE2W2LLl1PxOD1tEOsPRR57TtvtFaiAS/+0HvYldjiIgajRBPe8Me/4/oT7+mzGz73O+jiFcTfb3LBQDd+/rcJnrqWIKjXie1/N2Ii4j9AIy5+4PmtbqQ3fu7jjss+9oWvEbzzqi3cuNapMJf81u0OqR/85N+4IJ8VN2xwHgvFCay6aQPBQnvYefiA+xQTZ77hw7cRPHQhOvy1tuXbj9n1n3k/wVarICwveQTcLZCHemr/OOK+kP+iD1xn6955mT34+38P4ezzZh8s9V1yW77zDH3O2N3f/O+0ecCe/Iv7YRZvuX0Bzn2YW69f9U0jS0HEShCdhZugm1WXh+2GqxpAsgxuvLjVs08jglguDsu+Jms/KfuG13PBoj6++K9n7eKrLB0nzj5uE0TcDXJ+fzX6fBkhHiW49/rztW2cSMd4PwRA28a9nYaFxQQIIW2IgJQRBBVEFWkmjLg0guiPlFXOGYPx/Y+zIWnQqudVEFMQR0QPW/sREBmrH+eCWCMRngEccR1tPZZGyhyE+BZDVAyXZEVJxBLUn8qy27aozU70jFhZUbUtvaTKTmzrc94BZwfRePgoJFp9GRkhrmL2QslwV0DwT+3StcTIv8ImkYuh7hJ/srbkiltBMg7vePT7FJtAROfwDhmzznCpPqdKcM+g73Y9/7iNs186wJ6APIjC4J5dNtbbbQ3X3eK2C4sAOX16Vp1aGCeyYKSRP1WXdvV59eNewRagRVKwUnGNt+FjsKMTYlLmOL42zzQQKej0acYSLSpyCBSG4IQR4zIYUrQFeKAtbU//5d+6+qUGqNy6d91iW791Hx6OLY7bn/ue25whxnEHFlz91bO2GWv/goAgMThs277zoOQaQo5Pwn05/QeOLeRRfuVxF5Cqfvcd1Uk7RAVqvnhVuWAeUlC7IwAKF5bOLf95jFBQb3+AV1wBQbUrFthdX/u0dRL11sdusG3ffcxe+j8PsF7nYm3Gp8Slvfza2OM6lGvT2Sd4p7tCgXU1rF5AX6sQ9ytciK9L9OgE7abZSNTnVBDlSQx4brSBE2wuYgz/9pfazGHsrMaVKuOXRO2KCmxQmr/sdmte3GkDbAzKjMLBgR/+YyCcsB4ChWLFC8lb5Ti35z+ndta1v7+PNElEYVx9BNxw9FfHKOI9nqooDEVdiKUR0cMEuzEPQwkIQAp3HBJbWlZ/jHsVbIoS61cYrvb2N+NKrItyluBYp/UdfBLDXLcNEKhUISZGfT2c71fMyVc6XTiC1FxbVQaMdEAwovwoCEZf7AIFRG+Ow8n7xfwKURMLyuhbwK5YvsrWEkmYzBbbpvZvWfFQDKLEqULsvJQqEMR2MThEKLh+kWjWvLmvAtDGdZfBPXe4WACH+IhHkENb/rZ3stgBiMA/M24o2fkbAeozSwJ+G5rM2KLl6Cu9ljjZjmQBFVu8zMLoKwL8M19MBIRI+wMOv/wSovRmx+UPPPu8QyjF/DvVhMnzEVIHgshTANlzBGIEnUjhwQzALazaU590CQmE8LXLFtmqW6936ka8t9d5GajQbv7iHzikfvZ/f41Q5s3sPrzKlZMbz7XLN7VLhS79Fc4BWHbtRltxw2Vw3ac4S+Coy6f2lF+2CZ0epEslCkHsAQiFjoxGeiMsdog0gEaX6nT1MuO6T0M2ReEd37aHsxt2E8p8Jf1vcfsChjp7nUTk+kQVzoUHAVKEnBBZh3+M9nkbY1RlPq4sXf3H2dIb5x3IPRupJUqLkPQe6cSOwFZa9gfoKiJ23R+3S/gZ/vyrkQ3GhHnTCgvqrWkh5w3ko3Ozo2bbnu020V9ujahqPcTPJ1jbMgJ65F9XhN7wMO4zSaaiDKyMVASd7efUBCzobAWEYWCRH4GxQE/LiMxLcdbFQED2GoxqBPdMZHVohzb2sCUcAh0GXwq0JQ+xX6rIxASeAhjL8MmdNtS6jR8qQdPNY3NPNG0n2gecfp+gXB6bZlMg6nmLmznMFOMeBKeoLN8Gcf3FIGghTkRKwgzTLGAFxCITLLDrGmst0tcGIVsDAbjAFqx61DY/0uWIisxGCibSz4h5cMj3udZKBqeiynq2A19tRzY/AWdaBkciLl/cHuBbfu07HBDv5jhwLXzLhZfNVc0caVqVoBUtXmHRCrZIInqGcfelODD0bABI/Vr99tuIN9hvL/3d3zHZnjRQt3KlLb7qSqgum4tAKg1OBpHaFcudyP/qP3yDPQLziON/xi7/7Y+4hfWNaMI+Z7yDwq+4/mp79evfclKEVIruA4ftbX/w2/bKP97rELZhzQonfRSxzdfHwd2PPoel/GY3Vme550mII059cs9BCA6+4Tf3wzm7EKm7sPbGQKA2+vIK3P1cJwlIdF5y5Xp79Av/xzZ/axNSSpCY/AN23Wc+DLJ6+7q9yQQYGZf0cx9xRExaEf9f//6T9pMfPOWkJAUwrb/7BqQcLMzsLtT12Be+YW//849B2JfgQXnL7v3IX7ox6Z0kigUXr3RE6LmvPGhvPvQq+xL6J8coKcO1CZKce+el9vAff9u+fc+XEHFHgY0atlevIjIN6Pp1upggnXzbA5FrYiMNLJZ4/QGi5IgyZYdeO2dEROCEshNIpYuCfEubT2C4I54eQleHcS1PqmWcA0KAp2H05RIs6pwHQkgxP+HFbr4shjgZCRP48cfLz7VaAoOkhkoy1dmB2tob4lCcC+fNtw2EwmuG0qkR62jdbYnunejqXdbZNWJ1GFeDIDNKsHX2jlgp0kKA7/GRrC1rasQOjTqBnUc2h57eURtFly/GdpAFmQuRSsbC1RDwGseADh9+3YqThziXsMxiLes5gqAe92YbQUAYCgH2NFQgyJhlEBSRm7EZSIDrqD6DFHcvrW+xwc5jAOMWCMBygNc7SkvcsnrxSijeKPrfY+jXbF6YvxQAb7Wu/W+xr/9y8haxs20XImgJVuVmh+DaGTiODSAAtQopwAKDH6SVTRXEK3NSkPRzibrdB/eDxPzYyPKVDEohs+oXs8dC6Eix5vUXYomvAambMExdzXbbO7FIo6bwvpB9BeXNTVBgAln4NJ27FuTrRMTt5cCRa5EgsK5DOMqaGth4g6mWceoIstKGOvrZwPbiJvp9wBGBtXfeTP+LGet8uF0v6kEHiHoJlu81FMuSt9FJGBLvpWqUNtSgBmgHJT/ScM5StxtQVvfVt16NHaQKg2qxlbP5J0pfNR6J/NriW4HlvhBXYsOaJUgKxxxBuuB9N1t5Y41DqWLE8TIou/qqo8iUX8eXaT48aW0pZRe7WIKG1Qud3r/yhoudylA6rxI1bhEHbDRgkGzEmLjYifCVC+rt3HddiaejeVLsbzyXd3D4hRtXc3jIBdS5AK8H84Sfu2F1C+XnsSlqHkRkoTP6zV+/zC79+I2MGbgAoPxLSzVJofxEJbkX0xL8bHOlz5GmknPWcZq8+bjkQseP2Csvb7UXXniKHXPHkdVhEniWqoDLJAExCFDOc6P1DPDDG+WlIauuxDrOfvoI4rdsSoUE6cQ46qukSMFQY44oi5EV40cvASm7YgtspGwRhAMGqTMBgW95g4bZIIQJz1agViyK4rc/ss12v/6otR3eZtW4/Ub4+a9hpDHZGRrKkCSw/EfoUBHPWPQgJqjZEIcsur4Mdkfh/PoloHmkDbDzsBK7VlUVv3sYKuc925K3/cgCyQ4bOMkRdNle7GrYn15/mV8BQlVD/Sjih0OyeBskqLpQYB5mbAZyiAZya/GE5O7nvziZcMcDX3fAtvaOD7LY9VTi+fOlT+97ZhMupucxAr0HClXACcLf5QCKz6CHc1gBJwGVQPlaLtroGQHxAoxzVmCa3xAU1XXEBqCOEJOtjUWeqB4lCOUR14bOBJg6gViLr47RL4k/TvT2Vt6zQ0i0RscCuSddh0yW2vHEarlH4GRICM6HTT1SXVSlZxiUOI6nVxZjUXAuGSIF2BqnNshoYkSQXDw6Y3HWZfJKgnD9Qrx2un1u/mRc0zVBO54OLws0QEcf6Q0Ly1l1UgV4L/uA2gnQjvrkJBlENc2J2nF7DSRA8aw6JMF480Eil9qSBVqX66NvY6Al/50nHSH2wbmUU/OhTUkaj4tboG5Z/VW/ahUojCOBKE2qhtQIIYpXn9y4nqQl6UDP3iWxmcv98dOUQLpL07Mu752Xd2Y+vfXy+un+XelTz5N1TNY77R1pRXkjNrxpqz35SIL+h+3S86O2cUOVJcMEw6ATT4A0IqDz4LglhXBc1oHZd+MeYuddUpZ7xlsANy7EC7a/tdeSwE8RzUQx/pVg3CtkXsZKF9iuyguQGPAqpMO2BCN2iOg8uQq1Xbwxud+q08etmxOr2rv4MVDiEmSZHwCpCzACdrGRqLwICSBnP5LbTgZM/b5fMS5i+fv3HOvB4p9hu2+hdbJrMkWeCwkBHkWVgG6w8W7Emtkl2cr26n62Z69kB+YoUs3LHGTz5tYBtw+iiEhAwYx+hBTUcEs0pwqgBdAlQIoWl9qa2+5BLPwme7K/ZqtuvMuqF8GZ6YyAdvnbFLQTsF0P/zNiL8Y3JsxfGLWiBp2OTB4UIMtH389ggEthB1B6PkeBKZgHcHSr7vJqQdXD01zqVxrqLYR3SEC97k5+AbnSPQBUd3SoBm4yAU6uSif+qw19uNwxWq6v9ALEkvrgrtwseXXkRNxp/VI+T5WgYuZCIqd/eWVYGZIEtNIh/Q54RjwPURxiu0weQmqDxiTwq3+qV4ini+++muElTP1VPaiwLs9MJNGcEEtOFX66az9XVBFxbhpo1Bu7xuk6DffyxiNPg5uT3HxN1pfr91Qvfo2e6Gs6AUPgvL1QGJ86sBaD8BWDTGEs69lxkIgdMxGi8WS464HY50FwYfqczI+YD3GUWlPEL/4q9HcUghCB2E+QtxjjW5ay7Rjqguysm5ffg6sOwy+BOFHE8vn5xVZGQF0e6QcPPW7RdL+N0nYC1SOOha+xrADun+RsQX42DAJbqP0K3Otwb0fLI9Z2ogsrPicwIQGMQZxfP4HhFUItwt3JcxjPhs4hGE6MucjFCOrGOBJLF987OVthzcJafiRk2DpLMLofS7hfAYpTl36CLABRUUCS9gJoOc9IALScAooYx4Gf+86P2J4n77Pt938d49vltuCiqwm48cJ1l11zmzPQ9bcdReecD8eAw4EoIYx2R16uTr46AAAJqklEQVR7Hus0AdjTEEdUVWKPLuUT5/Mgl7+ka6/0osuv0FtlmePKQWLuzcxvc2T3k/yMs6pV8qwkv8Sp9xmZ/QpPzTaZMlcWP82/+5lnf1e6n+bfJ/POTpiW18/z7+CueZ9jJL94z6k4yNFYoXKOEpc7DgI8NF5nvRg/w1i/C4gDkBjfD3MIgJgL64osARJJNx7Gt0/UPsY43GUg9BibdZIQSklBoDVEYAJbQcoqQMQCkHeCnavzqjstVbxCNkK4PycPHf+Jndz/HFRo1MZi+oEOgoBG8NgQ3gunsHzU0wzEALoNAcpYdSkWPyQwqQDleG6QOeFZ2BFQY1IwW34UDP0dtZPTlyOoHnGkzggEoR9VgKqsGvXyyIkBm99YZnkYKUeyKX4DcQBpBkmbOsWsqyAifdzlCpT6ktAJQWcz0xKxdTbg2ts/aCfe3Mx5/884D0H9yvOsbvlaop3qbPFl17GSdJoOSzqQWKydgA34+IVe8hxopSUOv/Hg9zny691w5qTteeJhO++u93sEQVzarQtbIysrPbH3lwkdZ43lZ5iVX0YdZ6j+P179cmZAyySVKAkHLcUbFOQMLOLk4KJhJwKPIpovBFmq8d2LyY0izYVAjDjPklS1XduFCSEtSCNMc6x4EqSPAMfD7KkvRwoIsw8gg6g/Ec1YfbAfyzsn+2BQPbnnaes8/Bpn9iNtoO7l6Rc/OA5c0XtN+PNL8DqcQB3oOckxYhCQcfCmmujVAfYqlKDXF3K2ZBxjJKhqx/EKJBEei+hEPtJICV6Htq5+q6kpQaLIWAcHtFRWF1o1Bt969oAkkGSSuB734lpv52fAA9gthiESYSL/TjqyIronwyfSOC7EsyIAWhJ3cATcufn8y5wBsH0nJ7rs3mHHt7+KG6gWAxghstW16Eb4Ntn/XzKvESs0gRacByCfqvYVjLNBQnq1jHM630/YLt0siqQgsV12AB2cIXFeOrKkA1+0Vx9+PS6fIv2qKIHa/1W1/euxAlO9OPNc5MHli0H45Bi78WBIoQkIgXOBERRUxZFbhDLH+dkt/UR3XMY+LPtyryWJ4Q+gNcppFwEWx+HYvLJYKSG6cE2hTSXlh1NY5PtT1gz31o+Ppgb7rH3vU9bZthMCQZgwxtMxCAIUAqSjXgKAFCEof/wJkF9HlY2CxGXE7ZdwSlMPAUDOc8GhoIq4zcC5i+D4lRCdGlzCXcSWZGGSeZQL4laMDxCVK8YKgehHvD9JcFME1XhJU5lt6SGGEImiuz1lQ73sT+CsQWmowiypdIVsDS6tREKamsyzeAIhhcD5xWXE4t/CLrbLsYwfwbq5j/PwjuAB2IluIUXU2D78HqzFF2JowthC+OJQ5wnOpHvMcXg0d0JqCZIRHNOr7fffS3oeUsE7ICTznHFK41LaL/Xyq/tF8OfMMPdL7e5/VPaLzgDbY7McuAnMygSj6LeEftUHY1hjfSkqAXo/PxcUZRddmPj7DuICKvlBEjy3ROXBORHbq2NY2PlxjyIMgRmQPEo0YAKVQO66ghibgWrzcS/yA96HDvNT3m/aaG+Pza+tcy6/ESSGQd4VQWgq4epJYgOieBMOcRxbiqjWMESiGERswvOS5HtTLW2liK9ATamW1FLAbz6wcw+MtV7UmAGsfVU12M9of4g8hQQFRak/A/Os5hi19nbOdcRDkGLrYx5bkQv5KfHjhzD69QUgNhgAYeBiqiIAwwPYP5AOtO3tDAzFw5hJfMk9uKAIDFMhzjWvWbGW8/HWgbSKYU5AuRLOolwgYx9c3+EL91LCgc+76x6nf8gyr0tI7ox+UCXhegBKrCAMn+t7rc8GAvVXHZl6S2mvndlZZ3+fHEjuBfVMJalev0DuwbXjp02V8VM8o1quHyoy1SU/i1fn7HQ/r3/3c8/+rnQ/zb+L3Lt0EhyB1AuXMJV3aiC5d/+2N9ejqT+5xv1++n2Z/n3W8+z58ouccp9ebuZLcB1Y4sc3iforRhKVVDo2Lm9AGWG57KzjxzT0E1068WcIEb0E2JPInoTDl+HPHyB6voTDOUYlOaA7p7WfGBiPYfkfIUgqACGIcTqwHD06pWew+wQdwEZQUIJuDtGh3nzc2jIkyl2nfRyjwVHrbCd8nXP/ljXW2wASxACb0RJJfg6c+iaorI8zA3RKUEVlkXVw3P0wVv5aiMfIGD9QAhWLD7BDQK5DpJt8PBcxDh6R8bIPL4DCj6mGsRGgt6jahnfqzEFUB7i/x1DpIuCjnyx3x7lxeKlOfaDffDTpDqD0oEv3KRF8+mtlcx/JFFATZdWvCUWIEwggxntILUDlvcNyISjiBvuUhdye9V6veJ72XYvm2sm1zu0UUFZ+WWKZaxbBq9c1I9EoV053r6AHIMru9WXyRa4hXkyDIZdPef00V46suss+oXSvMu+7a1F1cnk0zXv+aX9z9U4v7or46dPL+2n+3W9I/ZlRiO+Tefx3yuA9S/Sb65os4l7qm+qZmTpXudOneevg9WVmPXNVO1ea6vZK5vqsGwmTtbnv/JmdroJ+mrvjZYKLBvSrOSByIwFcLXXszuOk3jhWcLnwZOg73q6fTSPGAp2eH7Rj+AVWBMHoCqVsdGLMKvhZoQRHABdUaCcgEXlgmdyxEbhqku29CazvQdx/URgd4S8u2Eb2BNwGFs7CpcknIqIAnhK4djmuvELCu3s7+Em3cL4Vs7sz0TNkcYhFAoV/KWc8dhA/sp9fdVrUXI7vn1OEkCnysR3AH4EAfpYMKQa8JjYBj0Iqbt3jJy3SgFfKkCKwLA724oLE+4Pgj/gv7u/NjSQAzWQeNgl5jfM2Pf/6SZ/jejM8babdjHt+Uc2tAwz/de6dyrhH/aURP9k9+e9yq+xuLgNDcHfXl6kyucKus65B1ZKrhJsG7mX2yvltTCZPb8dl1GD9vN6jjxD+t+l317xLyJXzX069IGXWOz/P9HKziMFU8TOVnVHRrC85rp9LnaqPBPflTPVOvZtRTnX9DGVd07MqmJuoTLXnykz7M7nmLu10+XLps9pSES/pdOWmNeQyA/gAeyk/PNJL9Nw4mNNSjw0f4FI/tHNO1no203EKD6fogiQROPDQRAJujE1A+wXg+vJWhbCYBckrO8C4fh2Y+AHF05eAxIoQVDyFTgYuxcWmSDsdKlIEso6BuCPUXQCHjkJsUqQPI7qrzhhqQQopQTG6RRAF+f3zJYFgM4jhquzgxGVZ/cuJPgwwDoX7hujAmFzzuC7D3MdgoPKgaTtznN8VCMQmrDLAUesQok5CyXUUWGJULmjmDpgU7ssboHl0zBeD/f8DCNGXWM2TfMgAAAAASUVORK5CYII=
\ No newline at end of file
diff --git a/operators/assets/icon.png b/operators/assets/icon.png
new file mode 100644
index 00000000..13312173
Binary files /dev/null and b/operators/assets/icon.png differ
diff --git a/operators/assets/operator-install.gif b/operators/assets/operator-install.gif
new file mode 100644
index 00000000..30efb81a
Binary files /dev/null and b/operators/assets/operator-install.gif differ
diff --git a/operators/bundle.Dockerfile b/operators/bundle.Dockerfile
new file mode 100644
index 00000000..90e23bd2
--- /dev/null
+++ b/operators/bundle.Dockerfile
@@ -0,0 +1,17 @@
+FROM scratch
+
+# Core bundle labels.
+LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1
+LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/
+LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/
+LABEL operators.operatorframework.io.bundle.package.v1=geostudio-operator
+LABEL operators.operatorframework.io.bundle.channels.v1=stable
+LABEL operators.operatorframework.io.bundle.channel.default.v1=stable
+LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.42.0
+LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1
+LABEL operators.operatorframework.io.metrics.project_layout=helm.sdk.operatorframework.io/v1
+
+# Copy files to locations specified by labels.
+COPY bundle/manifests /manifests/
+COPY bundle/metadata /metadata/
+COPY bundle/tests/scorecard /tests/scorecard/
diff --git a/operators/bundle/manifests/geostudio-operator.v0.0.1.yaml b/operators/bundle/manifests/geostudio-operator.v0.0.1.yaml
new file mode 100644
index 00000000..6f455dd8
--- /dev/null
+++ b/operators/bundle/manifests/geostudio-operator.v0.0.1.yaml
@@ -0,0 +1,299 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+ annotations:
+ alm-examples: '[ { "apiVersion": "geostudio.geostudio.ibm.com/v1alpha1", "kind": "GEOStudio", "metadata": { "name": "studio", "namespace": "default" }, "spec": { "infrastructure": { "postgresql": { "enabled": true }, "minio": { "enabled": true }, "keycloak": { "enabled": true }, "csiDriver": { "enabled": true } }, "global": { "namespace": "default", "environment": "production" } } }]'
+ capabilities: Basic Install
+ categories: "Developer Tools, Integration & Delivery"
+ description: Kubernetes Operator for deploying and managing GeoStudio
+ repository: https://github.com/terrastackai/geospatial-studio
+ containerImage: quay.io/geospatial-studio/geostudio-operator:v0.0.1
+ createdAt: "2026-02-23T00:00:00Z"
+ support: IBM
+ name: geostudio-operator.v0.0.1
+ namespace: placeholder
+spec:
+ apiservicedefinitions: {}
+ customresourcedefinitions:
+ owned:
+ - kind: GEOStudio
+ name: geostudios.geostudio.geostudio.ibm.com
+ version: v1alpha1
+ displayName: GeoStudio
+ description: Represents a GeoStudio deployment with integrated geospatial AI capabilities
+ resources:
+ - kind: Deployment
+ name: ""
+ version: apps/v1
+ - kind: StatefulSet
+ name: ""
+ version: apps/v1
+ - kind: Service
+ name: ""
+ version: v1
+ - kind: ConfigMap
+ name: ""
+ version: v1
+ - kind: Secret
+ name: ""
+ version: v1
+ - kind: PersistentVolumeClaim
+ name: ""
+ version: v1
+ - kind: Job
+ name: ""
+ version: batch/v1
+ specDescriptors:
+ - description: Enable PostgreSQL infrastructure component
+ displayName: PostgreSQL Enabled
+ path: infrastructure.postgresql.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable MinIO (S3-compatible) object storage infrastructure
+ displayName: MinIO Enabled
+ path: infrastructure.minio.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable Keycloak authentication infrastructure
+ displayName: Keycloak Enabled
+ path: infrastructure.keycloak.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable GeoServer for geospatial data serving
+ displayName: GeoServer Enabled
+ path: infrastructure.geoserver.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable CSI driver for S3 bucket mounting
+ displayName: CSI Driver Enabled
+ path: infrastructure.csiDriver.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Target namespace for GeoStudio deployment
+ displayName: Namespace
+ path: global.namespace
+ x-descriptors:
+ - 'urn:alm:descriptor:io.kubernetes:Namespace'
+ - description: Deployment environment (dev, staging, production)
+ displayName: Environment
+ path: global.environment
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:text'
+ - description: Image pull policy for all containers
+ displayName: Image Pull Policy
+ path: global.imagePullPolicy
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:imagePullPolicy'
+ statusDescriptors:
+ - description: Current deployment status
+ displayName: Status
+ path: status
+ x-descriptors:
+ - 'urn:alm:descriptor:io.kubernetes.phase'
+ description: |
+ ## GeoStudio Kubernetes Operator
+
+ The **GeoStudio Operator** is a Kubernetes operator that automates the deployment, configuration,
+ and lifecycle management of the Geospatial Exploration and Orchestration Studio on Kubernetes
+ and OpenShift platforms.
+
+ GeoStudio is an integrated platform for **fine-tuning, inference, and orchestration of geospatial
+ AI models**. It combines a no-code UI, low-code SDK, and APIs to make working with geospatial data
+ and AI accessible to researchers, data scientists, and developers.
+
+ ### Key Features
+
+ - 🚀 **One-command deployment** - Deploy the entire GeoStudio stack with a single YAML manifest
+ - 🔄 **Automated lifecycle management** - Handles upgrades, rollbacks, and configuration changes automatically
+ - 🏗️ **Infrastructure provisioning** - Optionally deploys PostgreSQL, MinIO, Keycloak, and GeoServer
+ - 🔐 **Secure by default** - Manages secrets, TLS certificates, and RBAC automatically
+ - 📦 **Helm-based** - Leverages proven Helm charts for reliable, reproducible deployments
+ - 🌍 **Geospatial AI ready** - Pre-configured for TerraTorch, TerraKit, and IBM geospatial models
+
+ ### What Gets Deployed
+
+ A complete GeoStudio installation includes:
+
+ **Core Components:**
+ - **Gateway API** - RESTful API server with OAuth2 authentication
+ - **UI** - React-based web interface for model management and inference
+ - **MLflow** - Experiment tracking and model registry
+ - **Pipelines** - Distributed geospatial data processing and inference orchestration
+
+ **Optional Infrastructure** (can be enabled/disabled):
+ - **PostgreSQL** - Relational database for metadata and state
+ - **MinIO** - S3-compatible object storage for geospatial data and models
+ - **Keycloak** - Identity and access management (OAuth2/OIDC)
+ - **GeoServer** - OGC-compliant geospatial data server (WMS, WFS)
+ - **Redis** - In-memory cache for session management and job queuing
+ - **CSI Driver** - IBM Object Storage S3 CSI driver for bucket mounting
+
+ ### Prerequisites
+
+ - Kubernetes 1.24+ or OpenShift 4.12+
+ - Storage class available for PersistentVolumes (at least 100GB recommended)
+ - (Optional) LoadBalancer or Ingress controller for external access
+ - (Optional) GPU nodes for model inference (if using GPU-accelerated pipelines)
+
+ ### Quick Start
+
+ After installing the operator, create a GeoStudio instance with default settings:
+
+ ```yaml
+ apiVersion: geostudio.geostudio.ibm.com/v1alpha1
+ kind: GEOStudio
+ metadata:
+ name: studio
+ namespace: default
+ spec:
+ infrastructure:
+ postgresql:
+ enabled: true
+ minio:
+ enabled: true
+ keycloak:
+ enabled: true
+ csiDriver:
+ enabled: true
+ global:
+ environment: production
+ namespace: default
+ ```
+
+ Apply the manifest:
+ ```bash
+ kubectl apply -f geostudio.yaml
+ ```
+
+ Monitor the deployment:
+ ```bash
+ kubectl get geostudio studio -n default
+ kubectl get pods -n default -w
+ ```
+
+ ### Access the Application
+
+ Once deployed, access the UI:
+ ```bash
+ # Port-forward to access locally
+ kubectl port-forward svc/geofm-ui 8080:80 -n default
+
+ # Open browser to http://localhost:8080
+ ```
+
+ For production, configure Ingress/Route for external access.
+
+ ### Configuration
+
+ GeoStudio is highly configurable. Common customizations:
+
+ **External Object Storage** (instead of in-cluster MinIO):
+ ```yaml
+ spec:
+ infrastructure:
+ minio:
+ enabled: false
+ global:
+ objectStorage:
+ endpoint: https://s3.amazonaws.com
+ access_key: YOUR_ACCESS_KEY
+ secret_key: YOUR_SECRET_KEY
+ region: us-east-1
+ buckets:
+ inference: my-inference-bucket
+ mlflow: my-mlflow-bucket
+ ```
+
+ **External Database** (instead of in-cluster PostgreSQL):
+ ```yaml
+ spec:
+ infrastructure:
+ postgresql:
+ enabled: false
+ global:
+ postgres:
+ backend_uri_base: postgresql://user:pass@external-db:5432
+ dbs:
+ gateway: geostudio
+ mlflow: mlflow
+ auth: geostudio_auth
+ ```
+
+ **Resource Limits**:
+ ```yaml
+ spec:
+ gfm-studio-gateway:
+ resources:
+ api:
+ limits:
+ cpu: "2"
+ memory: 4Gi
+ requests:
+ cpu: "1"
+ memory: 2Gi
+ ```
+
+ ### Support & Documentation
+
+ - **Documentation**: https://github.com/terrastackai/geospatial-studio/tree/main/operators
+ - **Issues**: https://github.com/terrastackai/geospatial-studio/issues
+ - **Email**: geostudio@ibm.com
+
+ ### License
+
+ Apache License 2.0
+ displayName: GeoStudio Operator
+ icon:
+ - base64data: "iVBORw0KGgoAAAANSUhEUgAAAQAAAAAnCAYAAADgrJZcAAABGmlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8nAzcDBwMDAy8CemFxc4BgQ4APkMcBoVPDtGgMjiL6sCzILUx4v4EpJLU4G0n+AODu5oKiEgYExA8hWLi8pALF7gGyRpGwwewGIXQR0IJC9BcROh7BPgNVA2HfAakKCnIHsD0A2XxKYzQSyiy8dwhYAsaH2goCgY0p+UqoCyPcahpaWFpok+oEgKEmtKAHRzvkFlUWZ6RklCo7AkEpV8MxL1tNRMDIwMmVgAIU7RPXnQHB4MoqdQYghAEJsjgQDg/9SBgaWPwgxk14GhgU6DAz8UxFiaoYMDAL6DAz75iSXFpVBjWFkMmZgIMQHAL7NSicB6KPMAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAEAoAMABAAAAAEAAAAnAAAAAPyfGMIAAAGeaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjM1NDE8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NTM3PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CnxOLvUAAEAASURBVHgB7b0HeFzXeef9Yiow6L0Qhb2LpBolkeqyerFky7bWjmw5jttu4t1kHSd+HMeOd51NWSfx4zjrxPk27nIsxZIpyeq9ssikKIq9giAAorfBDAYzg/39z52LRpCmS2Ln+XLJwb1z7unn7e97zuTd99Tmk3mWZ/qvK0/3gHvMJfFl8t2sfC6zCqmcnymXXd8ns+feqd5cvoCSXDKt5167B55P6U9ewMua65d655XJlc2Vn94Hl2FiwrXhV69yZqSd6co79f3M8tMKT77IetVOfp+Wh/amdXvqxRzteC9Pbd+lT9Z9mvfTxzWjwan8k1VM9YKnqfczkt2XM73T1PL+lEqZi9Nck+t8mvdestfmjLy5dZy72Nx9BGQskxy3wdYuhkifchUGApqcCb7SexXlTyDoDULfMxmXaAHeBwDSCRJVVKkqo+dsRj2Z8MrrkUvprs6sX59XNhzKs3SavOSZ8N95zVlRccgSo2lLj09YKBiw8XTG8gsCNjbmtZlVOdqP5Adoc8KiBWbRwqCNDeYapNYgfUyNZy2VzlqEtuiFZWlH49A3tavLjUN3PhWV+TaeytjwcNryLvnA5ybyNCkaAJU5JPKfNVcatF67u154+TRB7p27kYcBMGNuMlWfqzPXvLLqmqseVy8L4PLk2vDaUoFcG+pXrg+uTddPvVY5753fL78+16DaVxXuy+RUeK8m072vbmq8jCTMzEtzVEKaS869c3n9fNPq0KPeCZpydw3Df9ZrdwW8BfI6R15XVa7M9LqV5BMLl67SXv7Jfnk1eun8nczv0qcQcirdNeaVytXtukq9ronp7Xi5SM/1zeuoS51K01e/ztl15CpQ3ZP1TuX32vPLTk8/NU2dc2Nwr3Lv6b+rd0Ya9QBTya5+O/Dd5yxiRQ6ZR8eGLJlIOwQOhgOWTmUtFPJgNg0CCXayWd0pLnjWqEGmIIiVyXjvQxGzSFTLKwQ3EFcILgJjVhgLu/rD4aCjOSpbWxKyzsFxMkJcRACoNgqSNy0LWmlZxA7vHrWSyqANnwzY4FDKSspC1t+d1kRYYnjcCktDVlGfZ+FontUuCFpymHG1ha2A+tRmFsTojicAzzzLH6Mf3Pu4j/tERBXRzwxEawJCmEf+BUuKramh2J59ps1yNMObTMGCEM2baN1yX1SJEqncXRo834X0gVCQV0xQetwyoyOWSSUsm4ay8C4EyYoUxiwSi1kgEqZ8lv/0RDPsV6cqveZzdXPLNaMEAbm++h9lzhNV1j+6p6r0R4vn1ZvLqdv0emd/d6+nkMOrN9eQXzCHxLpNXtOe3Vy5OSGR/+67Mua64JVR3/Qyl67EXB1Kmrym9W9GusuQKzDjWWnTCqlSP8lvwOU/tTaXPO2Pn8O/e8Rr8tu0nGf36Hrr/syc35mlXYZcl71n733uWaB3ust1TX+UN9fPWd11MiPIXBgptZbq9SB6xA62breTRzosHIgCn4LZjI2lxrxWJgTHqjLX8GR9uQfeBwIhOHAa5CUbQ8ukhNDUQzsFhRFHUCLRIPAPRwcvMmBd32gGYkIdVBsKO5JpC1cErXlt0BJDGcdXahtDNgExaj+ONJCYsHC+WTKecZy/pMJs+fqotb2VtpLioKX7gjY0Ombli0KW6s+zEtpOQMQMmtFcGraunpS1JTIWyxe+mZMqhPQOJ4HDAASs7eiIjQ6nLKI+TSKNxulhkzch/sQq2f/kHgJC/CAdiA9ad9sR6z9+yAZPHLPkyJBlxlNeeRoXcYgWFltBeYVVNi+06uUrrGxeswXCYagwRMKvV/fcF4fHuXSvotl/yejyTi/t5Tk1ZSrdPSmDrhyMeY16SUrz+5BL8W6UccWExLqUSY9899Jd6r/yH9dSrg09Tw7gLNr1855F1l9Slql5md7v3PzOamMq78wXXslT+z6rRgrl1mFmcffNLRPAn59faGFgbiIdsFhBkc1vWeg4/eDgkLW2HpsqOb25ac8eA0AUpw6PtfOS/xlepBGlHT7QzhhMsDAasXEQXyJ2aZWYFJy9N2MTSBDBUMjGkUAG+yesvz1oAyeQBiIRe/OltI30jVntvJCVlEStp2vcJgoDVlaeZ6UVYTu4FSIC4o6NmpU1mA1BBDrbeUYqiNCHBCpAWUXI9ndPWBL+GoKtS2qRVJBBOoHtuj5moFoiRlIP+iAU4q4SJM7+AviDTEJioMdO7NxsHbu3W2p0mDnJWu2KNdZ03iUWLS5xecSRh3s6bfejD9D5iLX+5FXb//wTVt483xZtvNLqV6+BQLAonkJ19n34RXJOW9Sfu5pfRh0/d+P/UfBsZ8AnFMNDcduzZy/F8iyeHLEhkH7Xrl2OAEgv9kR+T4L09P0c6eGd7AX6OLUSeFbeMMwPDdt1QzmFaOEIenkihT6OSE6evLwssD1h+aVZi6ECDA+myWeWX4iOX4jaAQ6N9KetYVnYdr+StnFsFfMWRW0U4lBdF7IiSRUZuDvcPz6WZ2OgcCgAGofHraAEW8FYxqqqIvRtwoaHUD2KhPQT1lBEXyoK7a3WOD3LGgKOk6CduoKAExRBgFhEIyHLauzMyVkTgLw89BooSOu25+3oluc0nzZvzXqLlVXaW4/+wFrOv8wqFywhD5wdahhAD8ovr7S8xx60ZdfcZJULl1jv4f129LWXbNv3/smqFi211bfe4QhCFsr57+f6VVOAX3X7v04rdfq50JsAcJgYjVtX9xF09ZBlAoPo3RVwRbPBwQFbvHgxHBGxGpU1GkWx5xoaGrKCggIQKkSeQfde0oMQOxKO2nDyJEjU7wiDCEYA8TuYn28TsTyLxiKWHUvbwjVIHcUFVlgRhCgEEdfHkQQw4hWa9R4ZQbQ3O7E3a6XNoFF+nhWVF1oqELY03Pl4d9hqqostGi60TBBdfmDAYhXjjCFukeGI5cXyLVSTsngojRiftdEh+jmCYa8amwai/0Qf3H1E6koQ/T5tK88J2+E9tM/YRBBcn6lXhEz2i7MiABL3xxD39z55v/Ue228tF1xu8y+6io5V2cDxw65SyUSiKWpA1GciS5Nwd0kHe598yAqY+HBBzM59z9020t1pb2663577mz+zNXe8xxZdfuUvJAlocGd1+RmnwY2Spn396dX8zAV+epX/f86hufeX5Zc9DzJqC0FlFc/ClSWOx0fifBfHDNnx48cdggtmheDi9tLbfc6v/vjP3j2EPQuVQyowEkEAxI8uaLZgRQnIhh4vexCcu3U0z8qrllt/ugBikcEw2Ef9WOu7U3b4hR0WmBiwaH7Q9nxD9jKk6giqQ121xcoLQGaMii3UmV+O1JC1ozt3oWp3IsLnW9/+Esvug3owY8n4EMZICNwghAe1oW14wkZ74pZOMqPZAmcn0xhPtIH8qKv6J/Ff6CkDpwySUp5+KgGQHj/a3207H/qmZcaSdu6dH7bqhSuoKIMRJOUm7nQLF8ovsMZ1652BMDHQZ527d9qSK6+12uWrraJlgb31yAO2/QffseTQgK2+5XbqnG40Ol2tuXRBDYug66yBSBnnuFTLaV6dmntGRq/92ZkCcJWszK6qdXYWJQGYAsYJl2da6dl59cpP8+9+9tzY/a/unssjnVQLLb1zsvyMjD//F8FDVqzkV3UxRje/Z9EHLZVAaiyZRIw2K0anFsxo6oTo4uz+JQSfTgTGx8cdcZCEIBVAd+VR2RDWs7GRccsWVdh4tAZigBQB980LJB1K5QVi6PpyxSHZqgMgW3qUPgwnLYbrLxSNQYBoGQ49nuR7xHf1yfo/bsm+Ad6lGCceizFZ+DGwJyFefaOWX5LBeIjUUDcPKboRAjDKOveCi1JHRuhfGjyNkyfJwJEQ4uAoYr/zauTQS12S5yIIcTwjAchDjBgbHrCdP/onB0jnvefjVlRVC0Kn3KR4rj5/Cmfe5RUIQwDWvuN9zgZwcu9O2/Ktv3cLkGFyZQhc9673OgPhrk3/4hZ11S1vd8bBmTWd+i0YjjiJIUs9IUQ3qRuycv7Ui8VzlyDj571Ux4zy0FEBlUMMT5XpOdxqJbWVjB/RcGZmh/yJgWEAKG7lTbUg6lRlbj6p3+umly5X0qwGz9hzEZeBE93ODlNUVTqj/jMWnOslTYuD6hKxEuL3HG6HeNeyXl76XMX+tdIc4mJc68a3X7Wg2s3lmeZGPZSfvLi42HH8dKDf+dsdaWbeI9imhNhCdnFLrYWIwvDw8AzGpvUN815qsBAzhQ4u2l000W+xIhCpstrincdtbKiH+SqwgqoaJGb0cDwAMnFNQAh6Dx7AXpax8nkFFiyoBn6RSMJDtDNuaVx2Bi3iEUQfJe9B95xOokJEaItYgeEebAplafqPoQ+bQuWiFghS2On4o0OeyB9EUpd9IIQ2k49dIA+7QACVXHyAnnhjp40x1JRwBO7PGp6WAGjQWTq++/F/dhz8vHd91ArLq52VX0B2pkuGwsMvP42t4EWARkiB8QF3iwatSRYFRz1jQgO28oZbmKA00sCPrLiuzlrWX5IrM3cLQRDt6Kuv2t4nnrB4T48V1dRgS7jJmi+8gLpzJE5IRP9nI5/6ISA6JZ3E6Ujnt6w63ItpSOrqYPxSd1STxpAcGsYT0m715yxj4cbt9e/+yM6/6xarWjLfIY7rixqgnmAYseyNvXZ08xt2w2c/xuJDumknkxpHJfoW6lE/dUwRsw0fvRNby2LWwE9zFflddHdXv6MTuJ9wt+584AUIUIWd95+upX7PzeXWbMY4vCq8cfvV+fPjfRfy9x096ZChZnEDkuCwvfL1x+36T78bAxcKLfW5OSL7dELmSrupk8ipjp16+eVUx+zLvZPXZfor6hPRGe6K2xN/8bC9+8t3o1LKKj+79NR3Va21TjIHQeYvCGKMM7cS1YXoIyMjrn/e/HkVqb8iCiIIvvgvyWA0kSAdbk19QuSKmgIL17aYxcCJMenfiNXcJ7IhOKuMbVJ/QTBZ7+NZLPggptQRPBGBDHlTzA3EPZNSzAzoyd3IP56QrS2DPSGAfo+XgCEqZiFcALxR9+gQtoSiCJ9CpIcxKyootooltXD8QevCZgF6QBSAFYaThYGnaU8qQDoF8aBO4R36EGsqtQhpZmq6Zj5J7z+y5SkbaD9i577jI+j7NR53ngkxMwv53wDoJObJVHwY/f5aJiRMwwyc3hWUlln3gb12cu8u0vJs/sWX2qpb7rDB9hP2k+9/B0PiIiusqmQAOWT26+QuqeH469vsha98xSF+/erV1oFF9+m/+JLd8qf/w2qWL3WLlkZVGWfRC0pLqEYkWMEcYSYsiY6UBHhJZ5KVHmChE1iGBVz5xRhehGi5dHFp9TG/KMaCIrWIy0P6hfBR8uq7rr5jJ0C6x/FsLHX64TWf+oirTwRPxC5J/aH8iEUKPEOTykwigL5wCRja3zwAF4lb3erFrg9KFxDKXz0potJPEQsRDfVN/U4OxgEQuJmgRZfWKLdOei+gTlJvflHBlLTkxqj+AwjUH4ri1kVMTWORLsAopTLq+8EX3nRjrl3SAAwU281fuBuA9MYtYjaKBSrEPVIYZY48wHM+dp7TY9RVytzNEtdVPoUvezyZgqmoLdmMPATUOBNDowB41ImoWUTVAAggMTapgBcnhnuD01+vFA9zXALyKCpzw2LE+WjWhvuImMuDcAFagyBMc3OzxYhRGR0ddUjvGwO7u7udFKC+1Nc3gLisO2sZIQIonuyxijpiW+CgpU0tlqmtohMpIvjmWc8h4CxCPwf7LZWQ2xHuLd8dDcooGMW4aFjz5QIPBMNIJ2EXh1DWVGyV85c5YtB96CDv0pYaRpwP9CH+C28KQVbsBOUxVIB8R2DEgjA7WFl9vdU0NtrJw0esfd8e6p7IERyEChA/gMQgd2VAbkGQX6pAirUJ4wnALjk3AZAFcQQXXuu2Z7HuX4HIt8RxNsdF5pjo0yVFiAFYetWNEIIRgAEKio9UxsAjrzxv+5953KkG7Tt32MaPf8LOffd77Ykv/rG99fCDdvFvfoSBnEoAhFB7Hn/cidbXfvoPrWrxIhtoO24ncfOUYzgRUh3bstX2PPo4Aw4BsGV20Qfvdsh69NUt9tZDjzmkKaqutIs/dLdDyi3f/D6W2WMOwRduXG9rbr/Rtn33X2y4swtAHIbrDdjKG6+2FTdeZd37D6PG3OdwS4Rg40ffRx1R2/yN++FMPfbiV79lGz7yn+zlv78X1ed6jDolPH/fEQAB+5rbr7ElV10093QBzep/1aImu+Mv/7tDCh+8N39zE8anfFv//luQrHbYgWe32hWfuMte+OoPWGD8zO09Dnk3fvjt1nzBilz9HnEYaOuzl7724KSkseE3b4ZQzccT85QNdfbZYEePnXPrBtfeGz98CUKTZwUg+jW/dyfEdr/tfmyrI3QC4FU3XWjPfvlBu+q/vt1JMs//7Sbrb+12yHHOzRfaqpsvQLLZh4F3MwQlbL1Hu2zBJcvs4g9c5YiIOiaiuf/ZXfbGg1tc3wvKYnbN794MAcm3p7/0EO+ROlp7HAe9/g9ugyFUW9eBDnv6r37sCE9pfZmbFqbqp16amxCIKiSIIhKn4NLxHnzsIIS4vKz8kgKE+Pqu9DEYRwrblpBfaSMjw+6utRljDUMF6N9Y7uvyQ1Y6RPxLYYHFaxayPkUW7T2Jage6E2IbLsBGACdOoM9PpJEoeB8mKC6dRmdXsBHYl0Uwly1iPBW1/ArUgnGYTY+8DEjLg2JQ9DmZ52IK5DnIJzYghMsxJUkjD6IdlnyDiJ/G9hCQaoJ0A1sf6Q9YNJuwskIMnhAhD2+RLgg2EgGQ7s9Q3UcCwSmXqOzxHS+yiAX49i+nEl/8PCWrm6hTU3MpaoXrzU3ft2f+6gv29J9/zo5tBshAznwkgSs+8SmHeDvu+x5cvxoku5X3rwJUR1m0mcKJFmAcMWyos9MKKyutuLbWjm973Q4885yN9vVZ1779IGu/7bjvh3bJhz9oN/7JH7kBvvXwY5boH7TXv3e/rb/nvXbdZz8Flw7azh8+DJJ3W+vW7Xbtp3/Xrv7kf7HSefWOowye6KDshF3z+//ZLv3YB+yNf3nEhjpOOiRac/sNdsv/+kOrX7UMA+bDVtpQa+fcfq1VLWy2iz/4bvodtH4kAon2EtGazltlt3zx9+zCu2+z17//SI575+Zn1k3I0XO4zb73oc/Z937r8/bkn/2T68eK6zfa/qe3OMTf+u1HbPm1FztRv2vfMYhznd38Pz6KEfVSe/kfHiCu3FO5VLXmTMgvFeLWP/0o6tZF9sLfPeA4spB/oK3LNnzoJmtYs9Ah3FX/7Z12x5c+bsmBEdv39HaQd6X7LL5sta29fYPj2L2HZZEO2o4HXkGqGLXb/tcH7Orfvd223fs86kKX4+xC/Ms+doPd8Jl3QbC3004vZXKgxry6uf29W+1dX/4gCJO0fc++5SSQzn3tEMBau/Ov3o+OX2Ov3/eaQ/rnv/oksSNL7Z3/+702f/1CEBm16WwuiEQQhPEi4eB2SB5Cdt/YlwCeRACSSIVxdHZ9H8Dt5hMIzZ8QVGUcoiLWy4+/cGHQqqoLLJ2N27LkTluRxbKPETAULsUmELPCEmwOSEVCRiwNWPTh/rgICe6FeYzxTnmRmgoK4eoQjpiC6kaYiyFgPIU6ITUFNYB6wqWlwFglEkKTRUrqLFpab6VIyZGiUghVwHpOtNnme79nOzY9bPGOLmCv08YhWiEkBkmKCgQKI0EpwEj9URCQ0FLp2gMxE8vooiYrMQRCHXgDd99VUDZcD5gYNRmTFxU4kV4UEoqlGjVpMy/cEv29AOVfw6FarayxhU8ziPh/6XyxK1PaIEvmYuroRlQaw7V4Cbr9j+3QSy+AMB+wvGlBQn7t0tb17BCttdX2Pv4EgDhkK2+6kcVGpGJBD7/0Kp9XmNQ4QDmKynEIblxqNcuIU0CKWHTZBvpxn615x82Iu6WoFF+36iULUFcucUPQ2GpXLGXsMTwWi1E3qqjjCMhwvh18/lXb+u0fovOfyFFXqC2UXeJwfkkRCCjrLSIy81FcU2kDZcVw24foF9QY0cvT5afN5fRJY2AaQ+m8GlInrKi6HBsC/uKGKiSLd9rDn/lb2/iRd1jT+SsAlCRcJmp1Kxc6YFq4cY1tv+8pbAgDbm1kHUogng9iELzid+50hHrBJavoy5MQs16Xp2X9csrPR+xP2bxzFiLuv8FnJxGdCSdRqP4wqovUiyjqQwI1QkRKa922/RBE50LmKGo1qAdlTVXWvusYc1BAbEcVc1bqVImi6lJnqS5vqnTrJjBpXLfADjy3i7Z2s3YJG0cd0BUmfLVuWYNrr3Fdix18cQ9BZ6NIV0PEkqx2c1y/shFpASOwDxCu5Nx/BLLieGMgQmKUz8iEFRZWu8wS+2thIjIQ5mOsFeIXFqJXQ0AF6+3t7U4NUB7ZA0Q0QljXInm9Fsqg1gGb5Yj0xcBIUdsxjOV91rTuWhuIlBBok7HDO7ajsvRiBypD8sVgyb/eQ4eA8xELV1dZITaaTJL1Re+3IBtzaG8MxE1jxEtia5HaUIpUW0je4lLcjSVl6PFICLj/qoaPWXmqy7ohCG92TVgr8z46OAbhwUZBdRFCjmP0IZUA+XEVEsFMbXKD4mJ04j9GQ/CcIc1BAADevmP7HKLULFnrDHSzp1e6eBpr5f7nHrLW119ETFsKV/Ysm8orJCtrXGCN567nmUmvqIRTLbIFGy5HJyxnQTucLUDERnk14QIqRRE2n7+eOrcgLr8TQJDPM3ex4GEmvKS+Dl3rEFJCK7aDm61ifos9+rk/cYAptJJxZN66NU5q0T1WUQ5VbHULSEMO8NWmnqMs/g2f/SRRjbutfcebSChfsdv+/LPunRBQEyZAU/4w3oZXvv49J4Usu+YyV39/64nJvlMIqjSF2ELkfU+9DIF6kdiHmyw1Mkobe11+jUhjnkFUSZNaIc/ArV/8BO3KYEo0V85IKOOgVAqJ7J6LUbVwUQ/Y7fopDPPEPa/v7pnXmlsvnfzuGSghXfq1CIzUkx//yTdt4YZVNm/tQju5r9XrG3kdYdew9PEvnt3csLaTbfA86THgWes+Abdx12RZABBkfPiz9zrVoHHdfNoiplVjyF1S/dSmyrv58d/5Y1DWXLV+mdPdZUZSzDu1Ob1ZnLAfC79/nThxwj1K1BeHn35JBZChUPYAPevDCK25IWKFoXyLlGWsDANcFvXiWFfSguMjVrT1PsusutYmmhZ484KoL6OfYEGolobOaf9AJoVHATU7gJ9fSB3EHjCewd4UkldLIjqGRsR5bTqKQHBHO+MWQ+hZGB20pdEOdPgeq5RNqSZkB9sLkAhKrQqJoaeNPQAl4BuSRkFg3OLUrShFXZrPVBJDICqRbCPyFGheZkoAbnKJJIIAlNQ2gqQVLIaQxdXh/og79hzeg4j4IFRv0JZdfRt656UOAd2iAVtSGepXr0O0PBeu51m5BXiicAsvvZLBk4nGxS2nrNvqZJYy67APPInU0GY1S7GqT0oBAraALb/ueqzlf22P/88vwnmW0Ndjrl+ydlctWijIpF8jjtvvQiyqW7WCz3IMjPfb0Vc2k2eB7XnsKbwN58Ed23l+GhH+Jow0RRjhdru+C6jbtu+CgK2G2ByzeG8/lHyB7XzwMbwNa5EW5mNr2O7sA2PxUZrE6Imhb7AN9QSu7Yxh9Gr4ZA82jxJrwDtw9NXtOZvCkAPs0f4huNswRA3RjH/uPwvTte+o3fvhz+sr14Rd/jt3OeB7c9Nzdsdf/549+6XvYCd5CeJ3KYCUJrJyF4ShGMlpM20VWTHtC4mG4fICvOolTfbmj160dXdeyby+jqrliZQSD52dBQSTsS7eO0Q/FyJtVDB/cF0kCd/fP9I1gGQx6Prtj23hhpV4brZa3Yom3I49rFcfnH0hc4g9Bb3TvzxruDcaIXQGwB7pGbbGtZIIKzEWi8MPujIuL3CiS7Agy3VBaQHib5W9/oPNtv59G+zQy6h6SAU+XfDbmesug5iAXVtyZQQbBxeE7LrE6RXx5+v+vlqgd3pOQhRlPVc76rekBHkPZOEnkM7KMiVwcBC0MGTlpUQAToQhNsTzH3jWDmbGiAcKw2CAcQJzAiGs85Fy9HwiYzN4CUDCRNdhjHUDiPtsmCOaFkMBzAvpIUYEYVUJuFFoJTXgX/eQLQ912cpsHxsc86yCoKOeTKH1sGaNrGEdtGUM43eyb8Tyk0PsJsxaMXE1NfT72DgGY4ynKXAiQQyCRH6pACEIoSPsoGGwad2Vn/corRiYLM4pO7r1KTj2cvTaFY7b+JMgS3r3wV0gwjfpXAOGrnsQkde4ygTEqsfPK6SI93bBNf/GDr3wJCL5Mxj/nrNDLz7D96e5P404/TT6YStcuhKOcKmrJ4Sh8NBLz1tJXT3Au9QBgl8vKwPQNPKZ51yAowP9BBZdiahdQ39brH7NakTRRtv9yKMg3GYWJwbB2YBYiqukpQmAfRyE2QpAtSD+3+pEyq59B20vRODknv1EJd7kVIGjr21zSHd8207rgChc8N47IDYL6FO17XvyRewU2612pWcYlaGxomUeKsJR62/rQPpYAeL3QoAWMDeL7MSO3dgpXnOGyLJ5tU6CmLd2OUFRB52EUzG/wXFhLcgQNokCuLzUCc2DPo3nLscYt9vp/Q3nLMLYWUf7b4Ksi51BML84hp79KvMxiL3iDkcMQlh427bvQzJrsMWXr2MudkEgtgBwCbv0o7eDVIX0sZ95o+/N1bQDsBbl2xsPvIhX5agtvmKNM+7Vr2yhviIksgOOg1QvmWdDJwesEZtB3cpmEHmQMq/Cxdvs4nuusdpl8yBq+L9BunlrWtzaDXX0Yy9pos0Y64skhS4sg+L2f3nNEYslV6xCneohTyMENW4N3JU3SYCLYKlhTbPVr5iHyrDb9j69y70rn1eOpAiXzdkVlG/2JfjLoFoM7j0CAYATQ5PwauNGEzf2kFxILfHewT9pPqcXx5Per3SliUh4hCIPNYGxLYCLIzFk4bBGG2PEJvQQclxeEbWyslLbPoihzgpgfklHJAJhbGiBEhAbVQIDnijISDf6+lFsIyFclJEy5goJN1aMusA8hWKsHXPf12/Ljr9m59QlrbiqwE7GkRD4N0CQTxLGU1tXZmORYjs6zA7B7mF6jVo4jI0GO0Mp+4UV7z/M7qB8PDxpmEKkIEgYMs8YA3WJIORtuOfznAfAQPgv3VUhv6//85cx0lXYmlvvAXnQUxAvNBnepp4XQdAf2+Uf/2P0EgJNcGm4BaAOh6iMT9RbuuIwIb8vfOVPcfVdZqX1jY6bu8mmgCujZiE6sYoKJIZzXHl17Ik//bxD/ovu+ZAjSH69flkhhpsxiI5iDjwJBRLELOq76pZkoXwuDoGJULAQGdz3ILKVApVkTRCXlPSgOVCMgYDqmb/8qpMaVt10tRNZZTX13YBOPGXxVU4cQu3QHOOV0Qeg4J0XmORZXwVAipgUkmlLqOOqMDm5tiYo6ygx/dCl9lWZm5tcmripvqt/kqy0RkwZwJWxB37/r503oGphgzcHVKP65Td2CEAe9VFr4fdBa6M8EteZDj7kUdsQHY1RcyzDmfolbi/jneqSqqD5VTlfMhChkSShNPVJHFx9VRlPTfHa9st641Jb9Ie6XVtaF9nDKevsC8yf2nbA6eaLNgVb4l5IBCEAWJKm1wdv3vx6GY2m0V3KP4YU0/vE8yRnsIyDHkTnHd9HXW6SNfap/NOfVcF0xJ8iDEFrrA/ZdZehPrJzb4yjdOpLCm0Aq33/QMKq2dbbFWu0N+L1iBi8RyqcmCD2H+v9hNXQKtx4BA8HNrVETz9ErhsRPWbFDS0gK1uKS2qccTyL6lDTUGILdj5si5twW2oPAsbBgcGk1bLZJwn3D2AolEtwNFRk9+5BbWeskQiBAyc7LQ/7hkKNBYdD/WM8I+hrkblGOWNAZwtEIAqnRgIyk+lU0i30aF+X7frxt23t238TyoQPRSSKyyEh8zbY0WrhfvyaDM2lsUhafS1+cU09d20FLsJIVMiCR+Eq13pIRx4BlABG+fWsOkRIFEqtLZP52AKS+Gmd+sHb2Ze2HLs6KO+A1rVNddyFJF79AhipH+6/S3f1qEwufUIIzLM3Jg/g5ZstqauBI8acKKg+SIxSPZO6t9+uus4l6Uf90JMuHzgFqNorrv54qk4O4Lhl4BpUM+Py1SEvPZeXHAJOB6u8kHiaR73qT0Wz5tlNpNemMvn9zEnhKuuQTYQIpHOXy+Otp/Lr8t8pv1QCv3NOTaAuEUtX93TxHuT32qOunL6v8m78uXp9YuG14v0VsutybYHUIlK6/HnTs+ZOH10aM7gDVmrtpnuk1IiXR/lmX6p/pC9jtQTtjOfLEIheTfSfLqkApVjYxdnl9pO+P90dqLLKozS9k4FQ8fyRWNrKiwME3aBCYIwM1UDAOtJWAUEIcRZAf6TRijD6ZfEAifDlBQtRqXoJBe4Dt1Kowb10WVJFHEJLG6hEycEuYCtIGzFrWnw+rsg8a8KzEIoitlNXAilDaoe2+Q6NwKxoMg1MTtCGQovno1IM4BZMjeWzDaDGwvFWw7wAnIiQyY4lT0DY4hw4IsIQpp864CSLdDDDBuCmEy6mwS/aeBNbeJ+3XY98x9a8/R464xnkpIeLe26/7x/d4s+YdMop/v/Sj30KnacCClWG4e9KxLfH0bk3QBjqHKUTQghwHKIC1EJARxAEYzy78wKY+NOvLYW17uowlxCQGrwvP+PfSfBxbQsxxwlRvs3Vouc5q53W9mRzc6VNvpz18PN1dbISv89XfOI9rn8+t52zr7lSLI2P05P1/Do8/ExT8bPMMYMT8EvkHQGBtHmnsEiAD/Fxc4G/HI+RLsG7uLwuwZ++6y7ioHQRIEUCCnFTcPZMYZkVYvSLEvZ7vIPDNY5hf5pfZPnhAstHJAe9Qa4oSI0HJV8RiCLCbBcGI8MgrmL5qQpCy53vOr1HklgKhAxgO8CxYBMHWq00LkKBKg1zzMTT1t+PsZExxRhTAomgsrLcSvsG7ZqVDbanO2EJPAgxtgmPDbO7kHPDNLc6oGSMesNQkHzcje70ItI9NWCWG9DNL5goMbGouoEAkXvw9/4D233v5ZkIMETnmqVrQOQGh7yCKP67yRLSdh/YhXrwlObRXeLGLesvdfr/4ZeeBbF+A7FXpPw0F3U5yptAhNEmZ1V+msuJwuJ+gmxHDU6TcXYy2SVqqm6nHszVhJ+mqn/Ry03q6StR6K44v7iFu/y2/SJ+ef8+mU6Cmx+90OVJXyKqUkW0hr/IFVSkGPWP44WQyC53miz4Wp9/L5eYQlU122ubUJti49bfOWGHdzJTGgtMzN/2KwlAl4iDTwjcd6QdRQo6NYi50F4ACfNR4CeYjtlQW7+NZcIWKiFCj9N62mB+HQl0eP6NJ3GZ4u+PnxzEHcfmHtZjDN09m0YlUNBQcYVFEuwXIZAoSPxAOH8CI24luwGHrDiYskaIRj/uQUXtxTgNpFVRqcw/sUJgdcBaKvIJ9EEFoP4ohCWAO1H9SxM4pH3HE8kBd2qB1k2BUGMQhyh2AQmqKR0dxtxI8pohAQiZFPwj5EoSC9C4dqOtvuk37M2Hv8Xn21a9eLXbDFTRvJgKqESTAsAJDkUc4kRCTb8kesiyufadv0GyRLqcXMq3GXA+4wuGn3PWMBnlFDkViCWBqM1B/KYpdJ1C3HzFdbXM71Td0/sw4xnYla7ezhZLiezNF67LieYzcp35i4N/DwlESBz3PVucIJ+bL3EVqDpTYm/88En6cQ7uzSqIwFmMYY7eCUAlwvcc63A+/eLaMucNcPYJt0Yi6mdft8a1+9EtNtTeiwH0atv3/E6MlseIRHwbgC5r+NkOeI7O/hslyQBWgsGxugndGz4BLnFoB7vp8lFx6UOG+S8rK3NEQJKAEF0BQVIRRAj0UaxAPpKvgnYkBQRBxGiU0F6i7ibgqv0Y+fIwrDXjfSnjfL9dwQXoFpQfJY4igTEUxBtmv0o2ifSQDhFf0Q8AcFIQunu4oBQ842y/CuxoAQKFiiA+0QpCjZM2P33EglJNK4ocvOhQ0VJiMUqJsVDvs6x3CmLQc5QoTNLDEOn6pQ02kg2CgxzQg35v2iEoNy+DlRShE4rkEtQBomPYD2ATBC2hwsxYD3KHdXwSxybFsQFI1K+cv9xW3XAXPv9NhMzuxU98CbrnYg/wBVw04ER46fDTuI4mWcgqBGk45zyXR5uLlMabXLPcRT106Y4RQDrnqpvf7hbNATDIPnmRRwFDW775DTvChiC1F8KSu/rWW2zdu99BeRmPZFRChGOSHJLxLcACqnrpmALuzrf2ucirRZcqLNezG4hq63IqiAx61OUZEF0WV04GS41H7cpopgjBRZdfBFFUQIdHEL1K6AH51JYQXtRfBErf+1s73GagtXe8zRkfZZn3RHjXuOO26quzB2j1eBaCq3e6u/Zz+rbaEsdS8M+Tf/4tPA77lQSHybcL3ne9XfSBG7G0HyQYa5Nd+V/vZC3r3Lhc/TLC0Tdxdy8eAJsE9TtuT9r+p7ajAh4gRPtyAnLeRI3bybqsd1Z4AY/GwqDdnP7qCIK3Zm7Qp/kzjr2hgvnIJ7J0kNDZkXiPQwqtc2tr62QpjUFp/lg0r/IQxNHV9awPo0b81p4HNnQRRjwGxy1HUipD7N6bV2S941UWJcQ3TLTg+Ch78tnkU9FY4E7xlbsvCOceHx9lHRUpSGgwIcUhLP7lGBKjeBeQu60y1W6LxzsIOc63ahB2qH/E4kQZKlR6eARjMrkKamN2cFe3lRMLMB9bVYoArcLiWhubICCNeJNsAKIC400Tnl4IwZYrVLwUbAUuaQccKyIyUMbiGQRASBCCkxdV1WHkO0ohAfsEvvPVBKgsxjvwVTdJk7N2hgcZ845zDFjbDmK+efYvAZ/rCsDmLoc43qP+ahEE5EuveRsusPPpPJwyd8nCv2vTj3B/vWyLr7yC+IML8OM/Rvjv/Zw4NB9vw3q4VgfbJktAim582vVMehiE3+PFBixfgvpS5YDXSQK4+BShV7d6ubfI9ClJrMLJ3fuJVizEx72EDnmA0bX3ENSVeAC2YRbXVhFheBykeM35+VsuWudEeBkUZc0VoSkjmk/bgofau3AT1hMd1+Ci945t2Un48Zv4zJe5iL/l122E+suYCp1g3NopKAitZ0OQNhClMfSMEAknAtLf2ol7sYWgK6Izc8Y4EaKdDz7nkH8jLr7y5jrb/eNXWMNSpLg4fdyOS3A/cQu7XZxAvGeAMY/ixluA8SkOUWd7b3MN81KGP34A6egwbtYq17Yi87T+a2/fiHuuBeNouSMaCu3teOsYhCZCVOJi3Et4W+j7r9slA2yRtszCQbNw3n4iHHWun2yLivkvKcHlCsOQsU8nAkkV0HfBoC5JAcqntBgGOoXSMt0Wx6c+BDKWsSGsEgmhb2DcTtoCjuxigw+EpryiySrZsjAwMkhcSDF6PoXYuaNzCRJY/gPAlAzdQfbsFhHkf9H8xQQC4VkYPm7FB1l/du0l2MevXgyxi1BifEcHpxMBF2vWzrPDh3EXQ3jya4otjyjBAqT29ADjQpVME2iUZbdhtLLK8rrZIwLnFwGQxV+uQElG8jbFOGpcODCFmRqxCCqDVwzAwRcforO9HPmFaMpESsTXpW29jov/lPVWnoG2Y3CdxfjGL3AAIp0jjwnS/I52HHdIF2tsFl2iZj6ka3//geeedgE+Tedf6DQN1zAv5a5r3boVQC5DHH0/4lOZFdVW26bf/0N841uwN1yA2/Hv3G4/1Xfpf/mobfv2vRCzDpCmwnbc/6Bd90efpI0IMQmbnRjYc/CIVbzWaJf99m+hVuC2/PLfQ0xaQIYeAoe22qX/+R72DTxix7buYF4aiSV42q78b79FP3ZavLvXDr+41e0DGDjRSbDRJlyaZfRjLSclVSDeP447c769fu9DbD66EyJaTxzCDohTH2PcApe+hcCeb7Bh6Xb8vo3savwnjzjBmRX4c+2nP4Rffcge+aOvMofL0CuTbEZ6xG75nx9jPFB5QTJzpp11/hwpbPjaP7zbEYAj+P933P+ce/Xq//djR5R2P/oqROag/eYPvgChO2YP/dE/2nWffi/xAmvswU/9AwShwxEDcQkRcQHL3qd+AqHdakuvXsuxbh226TPfgDBhIccir7De2774fqRGhedqHX+2SyUE6P8aVxgYjMh+wQfF0UkBSW2YkaQJMsfZ/SiuKPFeabIL5BFDP/3SmBwhkFWfPONEEyUS41ajMwYwsA2NpizGbr0JiDWbA+DuSFWcOizio3JZnc7DpiAhorYKZ1ERqAanCe8gTBnFCEAciNyyiYPPUgXzyiEhCO42isBPJIFF1Tci+5bDSCIQNFqxlgWovcB4kNOKx1Ehzl1xno3T5o7AXusnBB4ss9Sxw65PRfQTxQXcxSvhiBsGQbwBI4xjJgEgk0TXiuZlIPkj7AfYaQsuJrTRie5BEGCJtb3xmrvPW3cxSD1zsqZPnJ5FBGTYKOb0Evm8GZOliR4cfGu7jbUfF4klhJIjjs8518IE66hz2l0X4xThydOFc5Wq39pckySUs4C8ol5p4uEjHDOm51GCJsSF0hColTffgHRwHpx/N0Eq++22P/sCrkyOTdq+E2DA6Ib4W714AYj8McfJH/38n4FECaSLxwg8WWnrP3AXxG/QfvTJzxMJeBSE2W2LLl1PxOD1tEOsPRR57TtvtFaiAS/+0HvYldjiIgajRBPe8Me/4/oT7+mzGz73O+jiFcTfb3LBQDd+/rcJnrqWIKjXie1/N2Ii4j9AIy5+4PmtbqQ3fu7jjss+9oWvEbzzqi3cuNapMJf81u0OqR/85N+4IJ8VN2xwHgvFCay6aQPBQnvYefiA+xQTZ77hw7cRPHQhOvy1tuXbj9n1n3k/wVarICwveQTcLZCHemr/OOK+kP+iD1xn6955mT34+38P4ezzZh8s9V1yW77zDH3O2N3f/O+0ecCe/Iv7YRZvuX0Bzn2YW69f9U0jS0HEShCdhZugm1WXh+2GqxpAsgxuvLjVs08jglguDsu+Jms/KfuG13PBoj6++K9n7eKrLB0nzj5uE0TcDXJ+fzX6fBkhHiW49/rztW2cSMd4PwRA28a9nYaFxQQIIW2IgJQRBBVEFWkmjLg0guiPlFXOGYPx/Y+zIWnQqudVEFMQR0QPW/sREBmrH+eCWCMRngEccR1tPZZGyhyE+BZDVAyXZEVJxBLUn8qy27aozU70jFhZUbUtvaTKTmzrc94BZwfRePgoJFp9GRkhrmL2QslwV0DwT+3StcTIv8ImkYuh7hJ/srbkiltBMg7vePT7FJtAROfwDhmzznCpPqdKcM+g73Y9/7iNs186wJ6APIjC4J5dNtbbbQ3X3eK2C4sAOX16Vp1aGCeyYKSRP1WXdvV59eNewRagRVKwUnGNt+FjsKMTYlLmOL42zzQQKej0acYSLSpyCBSG4IQR4zIYUrQFeKAtbU//5d+6+qUGqNy6d91iW791Hx6OLY7bn/ue25whxnEHFlz91bO2GWv/goAgMThs277zoOQaQo5Pwn05/QeOLeRRfuVxF5Cqfvcd1Uk7RAVqvnhVuWAeUlC7IwAKF5bOLf95jFBQb3+AV1wBQbUrFthdX/u0dRL11sdusG3ffcxe+j8PsF7nYm3Gp8Slvfza2OM6lGvT2Sd4p7tCgXU1rF5AX6sQ9ytciK9L9OgE7abZSNTnVBDlSQx4brSBE2wuYgz/9pfazGHsrMaVKuOXRO2KCmxQmr/sdmte3GkDbAzKjMLBgR/+YyCcsB4ChWLFC8lb5Ti35z+ndta1v7+PNElEYVx9BNxw9FfHKOI9nqooDEVdiKUR0cMEuzEPQwkIQAp3HBJbWlZ/jHsVbIoS61cYrvb2N+NKrItyluBYp/UdfBLDXLcNEKhUISZGfT2c71fMyVc6XTiC1FxbVQaMdEAwovwoCEZf7AIFRG+Ow8n7xfwKURMLyuhbwK5YvsrWEkmYzBbbpvZvWfFQDKLEqULsvJQqEMR2MThEKLh+kWjWvLmvAtDGdZfBPXe4WACH+IhHkENb/rZ3stgBiMA/M24o2fkbAeozSwJ+G5rM2KLl6Cu9ljjZjmQBFVu8zMLoKwL8M19MBIRI+wMOv/wSovRmx+UPPPu8QyjF/DvVhMnzEVIHgshTANlzBGIEnUjhwQzALazaU590CQmE8LXLFtmqW6936ka8t9d5GajQbv7iHzikfvZ/f41Q5s3sPrzKlZMbz7XLN7VLhS79Fc4BWHbtRltxw2Vw3ac4S+Coy6f2lF+2CZ0epEslCkHsAQiFjoxGeiMsdog0gEaX6nT1MuO6T0M2ReEd37aHsxt2E8p8Jf1vcfsChjp7nUTk+kQVzoUHAVKEnBBZh3+M9nkbY1RlPq4sXf3H2dIb5x3IPRupJUqLkPQe6cSOwFZa9gfoKiJ23R+3S/gZ/vyrkQ3GhHnTCgvqrWkh5w3ko3Ozo2bbnu020V9ujahqPcTPJ1jbMgJ65F9XhN7wMO4zSaaiDKyMVASd7efUBCzobAWEYWCRH4GxQE/LiMxLcdbFQED2GoxqBPdMZHVohzb2sCUcAh0GXwq0JQ+xX6rIxASeAhjL8MmdNtS6jR8qQdPNY3NPNG0n2gecfp+gXB6bZlMg6nmLmznMFOMeBKeoLN8Gcf3FIGghTkRKwgzTLGAFxCITLLDrGmst0tcGIVsDAbjAFqx61DY/0uWIisxGCibSz4h5cMj3udZKBqeiynq2A19tRzY/AWdaBkciLl/cHuBbfu07HBDv5jhwLXzLhZfNVc0caVqVoBUtXmHRCrZIInqGcfelODD0bABI/Vr99tuIN9hvL/3d3zHZnjRQt3KlLb7qSqgum4tAKg1OBpHaFcudyP/qP3yDPQLziON/xi7/7Y+4hfWNaMI+Z7yDwq+4/mp79evfclKEVIruA4ftbX/w2/bKP97rELZhzQonfRSxzdfHwd2PPoel/GY3Vme550mII059cs9BCA6+4Tf3wzm7EKm7sPbGQKA2+vIK3P1cJwlIdF5y5Xp79Av/xzZ/axNSSpCY/AN23Wc+DLJ6+7q9yQQYGZf0cx9xRExaEf9f//6T9pMfPOWkJAUwrb/7BqQcLMzsLtT12Be+YW//849B2JfgQXnL7v3IX7ox6Z0kigUXr3RE6LmvPGhvPvQq+xL6J8coKcO1CZKce+el9vAff9u+fc+XEHFHgY0atlevIjIN6Pp1upggnXzbA5FrYiMNLJZ4/QGi5IgyZYdeO2dEROCEshNIpYuCfEubT2C4I54eQleHcS1PqmWcA0KAp2H05RIs6pwHQkgxP+HFbr4shjgZCRP48cfLz7VaAoOkhkoy1dmB2tob4lCcC+fNtw2EwmuG0qkR62jdbYnunejqXdbZNWJ1GFeDIDNKsHX2jlgp0kKA7/GRrC1rasQOjTqBnUc2h57eURtFly/GdpAFmQuRSsbC1RDwGseADh9+3YqThziXsMxiLes5gqAe92YbQUAYCgH2NFQgyJhlEBSRm7EZSIDrqD6DFHcvrW+xwc5jAOMWCMBygNc7SkvcsnrxSijeKPrfY+jXbF6YvxQAb7Wu/W+xr/9y8haxs20XImgJVuVmh+DaGTiODSAAtQopwAKDH6SVTRXEK3NSkPRzibrdB/eDxPzYyPKVDEohs+oXs8dC6Eix5vUXYomvAambMExdzXbbO7FIo6bwvpB9BeXNTVBgAln4NJ27FuTrRMTt5cCRa5EgsK5DOMqaGth4g6mWceoIstKGOvrZwPbiJvp9wBGBtXfeTP+LGet8uF0v6kEHiHoJlu81FMuSt9FJGBLvpWqUNtSgBmgHJT/ScM5StxtQVvfVt16NHaQKg2qxlbP5J0pfNR6J/NriW4HlvhBXYsOaJUgKxxxBuuB9N1t5Y41DqWLE8TIou/qqo8iUX8eXaT48aW0pZRe7WIKG1Qud3r/yhoudylA6rxI1bhEHbDRgkGzEmLjYifCVC+rt3HddiaejeVLsbzyXd3D4hRtXc3jIBdS5AK8H84Sfu2F1C+XnsSlqHkRkoTP6zV+/zC79+I2MGbgAoPxLSzVJofxEJbkX0xL8bHOlz5GmknPWcZq8+bjkQseP2Csvb7UXXniKHXPHkdVhEniWqoDLJAExCFDOc6P1DPDDG+WlIauuxDrOfvoI4rdsSoUE6cQ46qukSMFQY44oi5EV40cvASm7YgtspGwRhAMGqTMBgW95g4bZIIQJz1agViyK4rc/ss12v/6otR3eZtW4/Ub4+a9hpDHZGRrKkCSw/EfoUBHPWPQgJqjZEIcsur4Mdkfh/PoloHmkDbDzsBK7VlUVv3sYKuc925K3/cgCyQ4bOMkRdNle7GrYn15/mV8BQlVD/Sjih0OyeBskqLpQYB5mbAZyiAZya/GE5O7nvziZcMcDX3fAtvaOD7LY9VTi+fOlT+97ZhMupucxAr0HClXACcLf5QCKz6CHc1gBJwGVQPlaLtroGQHxAoxzVmCa3xAU1XXEBqCOEJOtjUWeqB4lCOUR14bOBJg6gViLr47RL4k/TvT2Vt6zQ0i0RscCuSddh0yW2vHEarlH4GRICM6HTT1SXVSlZxiUOI6nVxZjUXAuGSIF2BqnNshoYkSQXDw6Y3HWZfJKgnD9Qrx2un1u/mRc0zVBO54OLws0QEcf6Q0Ly1l1UgV4L/uA2gnQjvrkJBlENc2J2nF7DSRA8aw6JMF480Eil9qSBVqX66NvY6Al/50nHSH2wbmUU/OhTUkaj4tboG5Z/VW/ahUojCOBKE2qhtQIIYpXn9y4nqQl6UDP3iWxmcv98dOUQLpL07Mu752Xd2Y+vfXy+un+XelTz5N1TNY77R1pRXkjNrxpqz35SIL+h+3S86O2cUOVJcMEw6ATT4A0IqDz4LglhXBc1oHZd+MeYuddUpZ7xlsANy7EC7a/tdeSwE8RzUQx/pVg3CtkXsZKF9iuyguQGPAqpMO2BCN2iOg8uQq1Xbwxud+q08etmxOr2rv4MVDiEmSZHwCpCzACdrGRqLwICSBnP5LbTgZM/b5fMS5i+fv3HOvB4p9hu2+hdbJrMkWeCwkBHkWVgG6w8W7Emtkl2cr26n62Z69kB+YoUs3LHGTz5tYBtw+iiEhAwYx+hBTUcEs0pwqgBdAlQIoWl9qa2+5BLPwme7K/ZqtuvMuqF8GZ6YyAdvnbFLQTsF0P/zNiL8Y3JsxfGLWiBp2OTB4UIMtH389ggEthB1B6PkeBKZgHcHSr7vJqQdXD01zqVxrqLYR3SEC97k5+AbnSPQBUd3SoBm4yAU6uSif+qw19uNwxWq6v9ALEkvrgrtwseXXkRNxp/VI+T5WgYuZCIqd/eWVYGZIEtNIh/Q54RjwPURxiu0weQmqDxiTwq3+qV4ini+++muElTP1VPaiwLs9MJNGcEEtOFX66az9XVBFxbhpo1Bu7xuk6DffyxiNPg5uT3HxN1pfr91Qvfo2e6Gs6AUPgvL1QGJ86sBaD8BWDTGEs69lxkIgdMxGi8WS464HY50FwYfqczI+YD3GUWlPEL/4q9HcUghCB2E+QtxjjW5ay7Rjqguysm5ffg6sOwy+BOFHE8vn5xVZGQF0e6QcPPW7RdL+N0nYC1SOOha+xrADun+RsQX42DAJbqP0K3Otwb0fLI9Z2ogsrPicwIQGMQZxfP4HhFUItwt3JcxjPhs4hGE6MucjFCOrGOBJLF987OVthzcJafiRk2DpLMLofS7hfAYpTl36CLABRUUCS9gJoOc9IALScAooYx4Gf+86P2J4n77Pt938d49vltuCiqwm48cJ1l11zmzPQ9bcdReecD8eAw4EoIYx2R16uTr46AAAJqklEQVR7Hus0AdjTEEdUVWKPLuUT5/Mgl7+ka6/0osuv0FtlmePKQWLuzcxvc2T3k/yMs6pV8qwkv8Sp9xmZ/QpPzTaZMlcWP82/+5lnf1e6n+bfJ/POTpiW18/z7+CueZ9jJL94z6k4yNFYoXKOEpc7DgI8NF5nvRg/w1i/C4gDkBjfD3MIgJgL64osARJJNx7Gt0/UPsY43GUg9BibdZIQSklBoDVEYAJbQcoqQMQCkHeCnavzqjstVbxCNkK4PycPHf+Jndz/HFRo1MZi+oEOgoBG8NgQ3gunsHzU0wzEALoNAcpYdSkWPyQwqQDleG6QOeFZ2BFQY1IwW34UDP0dtZPTlyOoHnGkzggEoR9VgKqsGvXyyIkBm99YZnkYKUeyKX4DcQBpBkmbOsWsqyAifdzlCpT6ktAJQWcz0xKxdTbg2ts/aCfe3Mx5/884D0H9yvOsbvlaop3qbPFl17GSdJoOSzqQWKydgA34+IVe8hxopSUOv/Hg9zny691w5qTteeJhO++u93sEQVzarQtbIysrPbH3lwkdZ43lZ5iVX0YdZ6j+P179cmZAyySVKAkHLcUbFOQMLOLk4KJhJwKPIpovBFmq8d2LyY0izYVAjDjPklS1XduFCSEtSCNMc6x4EqSPAMfD7KkvRwoIsw8gg6g/Ec1YfbAfyzsn+2BQPbnnaes8/Bpn9iNtoO7l6Rc/OA5c0XtN+PNL8DqcQB3oOckxYhCQcfCmmujVAfYqlKDXF3K2ZBxjJKhqx/EKJBEei+hEPtJICV6Htq5+q6kpQaLIWAcHtFRWF1o1Bt969oAkkGSSuB734lpv52fAA9gthiESYSL/TjqyIronwyfSOC7EsyIAWhJ3cATcufn8y5wBsH0nJ7rs3mHHt7+KG6gWAxghstW16Eb4Ntn/XzKvESs0gRacByCfqvYVjLNBQnq1jHM630/YLt0siqQgsV12AB2cIXFeOrKkA1+0Vx9+PS6fIv2qKIHa/1W1/euxAlO9OPNc5MHli0H45Bi78WBIoQkIgXOBERRUxZFbhDLH+dkt/UR3XMY+LPtyryWJ4Q+gNcppFwEWx+HYvLJYKSG6cE2hTSXlh1NY5PtT1gz31o+Ppgb7rH3vU9bZthMCQZgwxtMxCAIUAqSjXgKAFCEof/wJkF9HlY2CxGXE7ZdwSlMPAUDOc8GhoIq4zcC5i+D4lRCdGlzCXcSWZGGSeZQL4laMDxCVK8YKgehHvD9JcFME1XhJU5lt6SGGEImiuz1lQ73sT+CsQWmowiypdIVsDS6tREKamsyzeAIhhcD5xWXE4t/CLrbLsYwfwbq5j/PwjuAB2IluIUXU2D78HqzFF2JowthC+OJQ5wnOpHvMcXg0d0JqCZIRHNOr7fffS3oeUsE7ICTznHFK41LaL/Xyq/tF8OfMMPdL7e5/VPaLzgDbY7McuAnMygSj6LeEftUHY1hjfSkqAXo/PxcUZRddmPj7DuICKvlBEjy3ROXBORHbq2NY2PlxjyIMgRmQPEo0YAKVQO66ghibgWrzcS/yA96HDvNT3m/aaG+Pza+tcy6/ESSGQd4VQWgq4epJYgOieBMOcRxbiqjWMESiGERswvOS5HtTLW2liK9ATamW1FLAbz6wcw+MtV7UmAGsfVU12M9of4g8hQQFRak/A/Os5hi19nbOdcRDkGLrYx5bkQv5KfHjhzD69QUgNhgAYeBiqiIAwwPYP5AOtO3tDAzFw5hJfMk9uKAIDFMhzjWvWbGW8/HWgbSKYU5AuRLOolwgYx9c3+EL91LCgc+76x6nf8gyr0tI7ox+UCXhegBKrCAMn+t7rc8GAvVXHZl6S2mvndlZZ3+fHEjuBfVMJalev0DuwbXjp02V8VM8o1quHyoy1SU/i1fn7HQ/r3/3c8/+rnQ/zb+L3Lt0EhyB1AuXMJV3aiC5d/+2N9ejqT+5xv1++n2Z/n3W8+z58ouccp9ebuZLcB1Y4sc3iforRhKVVDo2Lm9AGWG57KzjxzT0E1068WcIEb0E2JPInoTDl+HPHyB6voTDOUYlOaA7p7WfGBiPYfkfIUgqACGIcTqwHD06pWew+wQdwEZQUIJuDtGh3nzc2jIkyl2nfRyjwVHrbCd8nXP/ljXW2wASxACb0RJJfg6c+iaorI8zA3RKUEVlkXVw3P0wVv5aiMfIGD9QAhWLD7BDQK5DpJt8PBcxDh6R8bIPL4DCj6mGsRGgt6jahnfqzEFUB7i/x1DpIuCjnyx3x7lxeKlOfaDffDTpDqD0oEv3KRF8+mtlcx/JFFATZdWvCUWIEwggxntILUDlvcNyISjiBvuUhdye9V6veJ72XYvm2sm1zu0UUFZ+WWKZaxbBq9c1I9EoV053r6AHIMru9WXyRa4hXkyDIZdPef00V46suss+oXSvMu+7a1F1cnk0zXv+aX9z9U4v7or46dPL+2n+3W9I/ZlRiO+Tefx3yuA9S/Sb65os4l7qm+qZmTpXudOneevg9WVmPXNVO1ea6vZK5vqsGwmTtbnv/JmdroJ+mrvjZYKLBvSrOSByIwFcLXXszuOk3jhWcLnwZOg73q6fTSPGAp2eH7Rj+AVWBMHoCqVsdGLMKvhZoQRHABdUaCcgEXlgmdyxEbhqku29CazvQdx/URgd4S8u2Eb2BNwGFs7CpcknIqIAnhK4djmuvELCu3s7+Em3cL4Vs7sz0TNkcYhFAoV/KWc8dhA/sp9fdVrUXI7vn1OEkCnysR3AH4EAfpYMKQa8JjYBj0Iqbt3jJy3SgFfKkCKwLA724oLE+4Pgj/gv7u/NjSQAzWQeNgl5jfM2Pf/6SZ/jejM8babdjHt+Uc2tAwz/de6dyrhH/aURP9k9+e9yq+xuLgNDcHfXl6kyucKus65B1ZKrhJsG7mX2yvltTCZPb8dl1GD9vN6jjxD+t+l317xLyJXzX069IGXWOz/P9HKziMFU8TOVnVHRrC85rp9LnaqPBPflTPVOvZtRTnX9DGVd07MqmJuoTLXnykz7M7nmLu10+XLps9pSES/pdOWmNeQyA/gAeyk/PNJL9Nw4mNNSjw0f4FI/tHNO1no203EKD6fogiQROPDQRAJujE1A+wXg+vJWhbCYBckrO8C4fh2Y+AHF05eAxIoQVDyFTgYuxcWmSDsdKlIEso6BuCPUXQCHjkJsUqQPI7qrzhhqQQopQTG6RRAF+f3zJYFgM4jhquzgxGVZ/cuJPgwwDoX7hujAmFzzuC7D3MdgoPKgaTtznN8VCMQmrDLAUesQok5CyXUUWGJULmjmDpgU7ssboHl0zBeD/f8DCNGXWM2TfMgAAAAASUVORK5CYII="
+ mediatype: image/png
+ install:
+ spec:
+ clusterPermissions:
+ - rules:
+ - apiGroups:
+ - "*"
+ resources:
+ - "*"
+ verbs:
+ - "*"
+ serviceAccountName: geostudio-operator
+ spec:
+ deployments: null
+ strategy: deployment
+ installModes:
+ - supported: true
+ type: OwnNamespace
+ - supported: true
+ type: SingleNamespace
+ - supported: false
+ type: MultiNamespace
+ - supported: true
+ type: AllNamespaces
+ keywords:
+ - geospatial
+ - geostudio
+ - ai
+ - machine-learning
+ - terratorch
+ - terrakit
+ - satellite-imagery
+ - kubernetes
+ - operator
+ - ibm
+ links:
+ - name: GeoStudio Documentation
+ url: https://terrastackai.github.io/geospatial-studio
+ - name: Operator Documentation
+ url: https://github.com/terrastackai/geospatial-studio/tree/main/operators
+ - name: Source Repository
+ url: https://github.com/terrastackai/geospatial-studio
+ - name: IBM Geospatial AI
+ url: https://github.com/IBM/terratorch
+ maintainers:
+ - email: geostudio@ibm.com
+ name: IBM GeoStudio Team
+ maturity: alpha
+ minKubeVersion: 1.24.0
+ provider:
+ name: IBM
+ version: 0.0.1
diff --git a/operators/bundle/manifests/geostudio.geostudio.ibm.com_geostudios.yaml b/operators/bundle/manifests/geostudio.geostudio.ibm.com_geostudios.yaml
new file mode 100644
index 00000000..7fb0caeb
--- /dev/null
+++ b/operators/bundle/manifests/geostudio.geostudio.ibm.com_geostudios.yaml
@@ -0,0 +1,44 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: geostudios.geostudio.geostudio.ibm.com
+spec:
+ group: geostudio.geostudio.ibm.com
+ names:
+ kind: GEOStudio
+ listKind: GEOStudioList
+ plural: geostudios
+ singular: geostudio
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: GEOStudio is the Schema for the geostudios API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Spec defines the desired state of GEOStudio
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ status:
+ description: Status defines the observed state of GEOStudio
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/operators/bundle/metadata/annotations.yaml b/operators/bundle/metadata/annotations.yaml
new file mode 100644
index 00000000..c00ae52f
--- /dev/null
+++ b/operators/bundle/metadata/annotations.yaml
@@ -0,0 +1,10 @@
+annotations:
+ operators.operatorframework.io.bundle.channel.default.v1: stable
+ operators.operatorframework.io.bundle.channels.v1: stable
+ operators.operatorframework.io.bundle.manifests.v1: manifests/
+ operators.operatorframework.io.bundle.mediatype.v1: registry+v1
+ operators.operatorframework.io.bundle.metadata.v1: metadata/
+ operators.operatorframework.io.bundle.package.v1: geostudio-operator
+ operators.operatorframework.io.metrics.builder: operator-sdk-v1.42.0
+ operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
+ operators.operatorframework.io.metrics.project_layout: helm.sdk.operatorframework.io/v1
diff --git a/operators/bundle/tests/scorecard/config.yaml b/operators/bundle/tests/scorecard/config.yaml
new file mode 100644
index 00000000..d8cbab8a
--- /dev/null
+++ b/operators/bundle/tests/scorecard/config.yaml
@@ -0,0 +1,50 @@
+# The scorecard configuration for this bundle
+apiVersion: scorecard.operatorframework.io/v1alpha3
+kind: Configuration
+metadata:
+ name: config
+stages:
+- parallel: true
+ tests:
+ - entrypoint:
+ - scorecard-test
+ - basic-check-spec
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: basic
+ test: basic-check-spec-test
+ - entrypoint:
+ - scorecard-test
+ - olm-bundle-validation
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: olm
+ test: olm-bundle-validation-test
+ - entrypoint:
+ - scorecard-test
+ - olm-crds-have-validation
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: olm
+ test: olm-crds-have-validation-test
+ - entrypoint:
+ - scorecard-test
+ - olm-crds-have-resources
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: olm
+ test: olm-crds-have-resources-test
+ - entrypoint:
+ - scorecard-test
+ - olm-spec-descriptors
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: olm
+ test: olm-spec-descriptors-test
+ - entrypoint:
+ - scorecard-test
+ - olm-status-descriptors
+ image: quay.io/operator-framework/scorecard-test:v1.42.0
+ labels:
+ suite: olm
+ test: olm-status-descriptors-test
diff --git a/operators/config/crd/bases/geostudio.geostudio.ibm.com_geostudios.yaml b/operators/config/crd/bases/geostudio.geostudio.ibm.com_geostudios.yaml
new file mode 100644
index 00000000..7fb0caeb
--- /dev/null
+++ b/operators/config/crd/bases/geostudio.geostudio.ibm.com_geostudios.yaml
@@ -0,0 +1,44 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: geostudios.geostudio.geostudio.ibm.com
+spec:
+ group: geostudio.geostudio.ibm.com
+ names:
+ kind: GEOStudio
+ listKind: GEOStudioList
+ plural: geostudios
+ singular: geostudio
+ scope: Namespaced
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: GEOStudio is the Schema for the geostudios API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Spec defines the desired state of GEOStudio
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ status:
+ description: Status defines the observed state of GEOStudio
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/operators/config/crd/kustomization.yaml b/operators/config/crd/kustomization.yaml
new file mode 100644
index 00000000..c60d7096
--- /dev/null
+++ b/operators/config/crd/kustomization.yaml
@@ -0,0 +1,6 @@
+# This kustomization.yaml is not intended to be run by itself,
+# since it depends on service name and namespace that are out of this kustomize package.
+# It should be run by config/default
+resources:
+- bases/geostudio.geostudio.ibm.com_geostudios.yaml
+# +kubebuilder:scaffold:crdkustomizeresource
diff --git a/operators/config/default/kustomization.yaml b/operators/config/default/kustomization.yaml
new file mode 100644
index 00000000..8cbe0bf2
--- /dev/null
+++ b/operators/config/default/kustomization.yaml
@@ -0,0 +1,40 @@
+# Adds namespace to all resources.
+namespace: geostudio-operators-system
+
+# Value of this field is prepended to the
+# names of all resources, e.g. a deployment named
+# "wordpress" becomes "alices-wordpress".
+# Note that it should also match with the prefix (text before '-') of the namespace
+# field above.
+namePrefix: operators-
+
+# Labels to add to all resources and selectors.
+commonLabels:
+ app.kubernetes.io/component: operator
+ app.kubernetes.io/managed-by: kustomize
+ app.kubernetes.io/name: geostudio-operator
+ app.kubernetes.io/part-of: geostudio
+
+# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
+#- ../prometheus
+# [METRICS] Expose the controller manager metrics service.
+resources:
+- ../crd
+- ../rbac
+- ../manager
+- metrics_service.yaml
+# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
+# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
+# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
+# be able to communicate with the Webhook Server.
+#- ../network-policy
+
+# Uncomment the patches line if you enable Metrics
+# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
+# More info: https://book.kubebuilder.io/reference/metrics
+patches:
+- path: manager_metrics_patch.yaml
+ target:
+ kind: Deployment
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
diff --git a/operators/config/default/labels-patch.yaml b/operators/config/default/labels-patch.yaml
new file mode 100644
index 00000000..0a00fa3c
--- /dev/null
+++ b/operators/config/default/labels-patch.yaml
@@ -0,0 +1,9 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+# Add common labels to all resources
+commonLabels:
+ app.kubernetes.io/name: geostudio-operator
+ app.kubernetes.io/part-of: geostudio
+ app.kubernetes.io/component: operator
+ app.kubernetes.io/managed-by: kustomize
diff --git a/operators/config/default/manager_metrics_patch.yaml b/operators/config/default/manager_metrics_patch.yaml
new file mode 100644
index 00000000..a3cb2f18
--- /dev/null
+++ b/operators/config/default/manager_metrics_patch.yaml
@@ -0,0 +1,12 @@
+# This patch adds the args to allow exposing the metrics endpoint using HTTPS
+- op: add
+ path: /spec/template/spec/containers/0/args/0
+ value: --metrics-bind-address=:8443
+# This patch adds the args to allow securing the metrics endpoint
+- op: add
+ path: /spec/template/spec/containers/0/args/0
+ value: --metrics-secure
+# This patch adds the args to allow RBAC-based authn/authz the metrics endpoint
+- op: add
+ path: /spec/template/spec/containers/0/args/0
+ value: --metrics-require-rbac
diff --git a/operators/config/default/metrics_service.yaml b/operators/config/default/metrics_service.yaml
new file mode 100644
index 00000000..adfa3733
--- /dev/null
+++ b/operators/config/default/metrics_service.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: controller-manager-metrics-service
+ namespace: system
+spec:
+ ports:
+ - name: https
+ port: 8443
+ protocol: TCP
+ targetPort: 8443
+ selector:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
diff --git a/operators/config/manager/kustomization.yaml b/operators/config/manager/kustomization.yaml
new file mode 100644
index 00000000..901413c9
--- /dev/null
+++ b/operators/config/manager/kustomization.yaml
@@ -0,0 +1,26 @@
+# Kustomization Configuration for Operator Manager
+#
+# This file is dynamically updated by the Makefile during installation.
+# The 'make install' target uses 'kustomize edit set image' to update
+# the controller image based on the IMG variable.
+#
+# Usage:
+# Local: make install IMG=geostudio-operator:local
+# Production: make install IMG=quay.io/geospatial-studio/geostudio-operator:v0.0.1
+#
+# The default values below are placeholders and will be overwritten.
+
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- manager.yaml
+
+# Default namespace (updated by Makefile)
+namespace: geostudio-operators-system
+
+# Default image configuration (updated by Makefile via 'kustomize edit')
+images:
+- name: controller
+ newName: quay.io/geospatial-studio/geostudio-operator
+ newTag: latest
diff --git a/operators/config/manager/manager.yaml b/operators/config/manager/manager.yaml
new file mode 100644
index 00000000..aebb04d8
--- /dev/null
+++ b/operators/config/manager/manager.yaml
@@ -0,0 +1,100 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ labels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: system
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: controller-manager
+ namespace: system
+ labels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+spec:
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ replicas: 1
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ labels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ spec:
+ # TODO(user): Uncomment the following code to configure the nodeAffinity expression
+ # according to the platforms which are supported by your solution.
+ # It is considered best practice to support multiple architectures. You can
+ # build your manager image using the makefile target docker-buildx.
+ # affinity:
+ # nodeAffinity:
+ # requiredDuringSchedulingIgnoredDuringExecution:
+ # nodeSelectorTerms:
+ # - matchExpressions:
+ # - key: kubernetes.io/arch
+ # operator: In
+ # values:
+ # - amd64
+ # - arm64
+ # - ppc64le
+ # - s390x
+ # - key: kubernetes.io/os
+ # operator: In
+ # values:
+ # - linux
+ securityContext:
+ # Projects are configured by default to adhere to the "restricted" Pod Security Standards.
+ # This ensures that deployments meet the highest security requirements for Kubernetes.
+ # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
+ runAsNonRoot: true
+ seccompProfile:
+ type: RuntimeDefault
+ containers:
+ - args:
+ - --leader-elect
+ - --leader-election-id=operators
+ - --reconcile-period=5m
+ - --health-probe-bind-address=:8081
+ image: controller:latest
+ name: manager
+ ports: []
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8081
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ # TODO(user): Configure the resources accordingly based on the project requirements.
+ # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+ resources:
+ limits:
+ cpu: 500m
+ memory: 128Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ volumeMounts: []
+ volumes: []
+ imagePullSecrets:
+ - name: quay-pull-secret
+ serviceAccountName: controller-manager
+ terminationGracePeriodSeconds: 10
diff --git a/operators/config/manifests/bases/geostudio-operator.clusterserviceversion.yaml b/operators/config/manifests/bases/geostudio-operator.clusterserviceversion.yaml
new file mode 100644
index 00000000..a158fe14
--- /dev/null
+++ b/operators/config/manifests/bases/geostudio-operator.clusterserviceversion.yaml
@@ -0,0 +1,289 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+ annotations:
+ alm-examples: '[]'
+ capabilities: Basic Install
+ categories: "Developer Tools, Integration & Delivery"
+ description: Kubernetes Operator for deploying and managing GeoStudio
+ repository: https://github.com/terrastackai/geospatial-studio
+ containerImage: quay.io/geospatial-studio/geostudio-operator:v0.0.1
+ createdAt: "2026-02-23T00:00:00Z"
+ support: IBM
+ name: geostudio-operator.v0.0.0
+ namespace: placeholder
+spec:
+ apiservicedefinitions: {}
+ customresourcedefinitions:
+ owned:
+ - kind: GEOStudio
+ name: geostudios.geostudio.geostudio.ibm.com
+ version: v1alpha1
+ displayName: GeoStudio
+ description: Represents a GeoStudio deployment with integrated geospatial AI capabilities
+ resources:
+ - kind: Deployment
+ name: ""
+ version: apps/v1
+ - kind: StatefulSet
+ name: ""
+ version: apps/v1
+ - kind: Service
+ name: ""
+ version: v1
+ - kind: ConfigMap
+ name: ""
+ version: v1
+ - kind: Secret
+ name: ""
+ version: v1
+ - kind: PersistentVolumeClaim
+ name: ""
+ version: v1
+ - kind: Job
+ name: ""
+ version: batch/v1
+ specDescriptors:
+ - description: Enable PostgreSQL infrastructure component
+ displayName: PostgreSQL Enabled
+ path: infrastructure.postgresql.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable MinIO (S3-compatible) object storage infrastructure
+ displayName: MinIO Enabled
+ path: infrastructure.minio.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable Keycloak authentication infrastructure
+ displayName: Keycloak Enabled
+ path: infrastructure.keycloak.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable GeoServer for geospatial data serving
+ displayName: GeoServer Enabled
+ path: infrastructure.geoserver.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Enable CSI driver for S3 bucket mounting
+ displayName: CSI Driver Enabled
+ path: infrastructure.csiDriver.enabled
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch'
+ - description: Target namespace for GeoStudio deployment
+ displayName: Namespace
+ path: global.namespace
+ x-descriptors:
+ - 'urn:alm:descriptor:io.kubernetes:Namespace'
+ - description: Deployment environment (dev, staging, production)
+ displayName: Environment
+ path: global.environment
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:text'
+ - description: Image pull policy for all containers
+ displayName: Image Pull Policy
+ path: global.imagePullPolicy
+ x-descriptors:
+ - 'urn:alm:descriptor:com.tectonic.ui:imagePullPolicy'
+ statusDescriptors:
+ - description: Current deployment status
+ displayName: Status
+ path: status
+ x-descriptors:
+ - 'urn:alm:descriptor:io.kubernetes.phase'
+ description: |
+ ## GeoStudio Kubernetes Operator
+
+ The **GeoStudio Operator** is a Kubernetes operator that automates the deployment, configuration,
+ and lifecycle management of the Geospatial Exploration and Orchestration Studio on Kubernetes
+ and OpenShift platforms.
+
+ GeoStudio is an integrated platform for **fine-tuning, inference, and orchestration of geospatial
+ AI models**. It combines a no-code UI, low-code SDK, and APIs to make working with geospatial data
+ and AI accessible to researchers, data scientists, and developers.
+
+ ### Key Features
+
+ - 🚀 **One-command deployment** - Deploy the entire GeoStudio stack with a single YAML manifest
+ - 🔄 **Automated lifecycle management** - Handles upgrades, rollbacks, and configuration changes automatically
+ - 🏗️ **Infrastructure provisioning** - Optionally deploys PostgreSQL, MinIO, Keycloak, and GeoServer
+ - 🔐 **Secure by default** - Manages secrets, TLS certificates, and RBAC automatically
+ - 📦 **Helm-based** - Leverages proven Helm charts for reliable, reproducible deployments
+ - 🌍 **Geospatial AI ready** - Pre-configured for TerraTorch, TerraKit, and IBM geospatial models
+
+ ### What Gets Deployed
+
+ A complete GeoStudio installation includes:
+
+ **Core Components:**
+ - **Gateway API** - RESTful API server with OAuth2 authentication
+ - **UI** - React-based web interface for model management and inference
+ - **MLflow** - Experiment tracking and model registry
+ - **Pipelines** - Distributed geospatial data processing and inference orchestration
+
+ **Optional Infrastructure** (can be enabled/disabled):
+ - **PostgreSQL** - Relational database for metadata and state
+ - **MinIO** - S3-compatible object storage for geospatial data and models
+ - **Keycloak** - Identity and access management (OAuth2/OIDC)
+ - **GeoServer** - OGC-compliant geospatial data server (WMS, WFS)
+ - **Redis** - In-memory cache for session management and job queuing
+ - **CSI Driver** - IBM Object Storage S3 CSI driver for bucket mounting
+
+ ### Prerequisites
+
+ - Kubernetes 1.24+ or OpenShift 4.12+
+ - Storage class available for PersistentVolumes (at least 100GB recommended)
+ - (Optional) LoadBalancer or Ingress controller for external access
+ - (Optional) GPU nodes for model inference (if using GPU-accelerated pipelines)
+
+ ### Quick Start
+
+ After installing the operator, create a GeoStudio instance with default settings:
+
+ ```yaml
+ apiVersion: geostudio.geostudio.ibm.com/v1alpha1
+ kind: GEOStudio
+ metadata:
+ name: studio
+ namespace: default
+ spec:
+ infrastructure:
+ postgresql:
+ enabled: true
+ minio:
+ enabled: true
+ keycloak:
+ enabled: true
+ csiDriver:
+ enabled: true
+ global:
+ environment: production
+ namespace: default
+ ```
+
+ Apply the manifest:
+ ```bash
+ kubectl apply -f geostudio.yaml
+ ```
+
+ Monitor the deployment:
+ ```bash
+ kubectl get geostudio studio -n default
+ kubectl get pods -n default -w
+ ```
+
+ ### Access the Application
+
+ Once deployed, access the UI:
+ ```bash
+ # Port-forward to access locally
+ kubectl port-forward svc/geofm-ui 8080:80 -n default
+
+ # Open browser to http://localhost:8080
+ ```
+
+ For production, configure Ingress/Route for external access.
+
+ ### Configuration
+
+ GeoStudio is highly configurable. Common customizations:
+
+ **External Object Storage** (instead of in-cluster MinIO):
+ ```yaml
+ spec:
+ infrastructure:
+ minio:
+ enabled: false
+ global:
+ objectStorage:
+ endpoint: https://s3.amazonaws.com
+ access_key: YOUR_ACCESS_KEY
+ secret_key: YOUR_SECRET_KEY
+ region: us-east-1
+ buckets:
+ inference: my-inference-bucket
+ mlflow: my-mlflow-bucket
+ ```
+
+ **External Database** (instead of in-cluster PostgreSQL):
+ ```yaml
+ spec:
+ infrastructure:
+ postgresql:
+ enabled: false
+ global:
+ postgres:
+ backend_uri_base: postgresql://user:pass@external-db:5432
+ dbs:
+ gateway: geostudio
+ mlflow: mlflow
+ auth: geostudio_auth
+ ```
+
+ **Resource Limits**:
+ ```yaml
+ spec:
+ gfm-studio-gateway:
+ resources:
+ api:
+ limits:
+ cpu: "2"
+ memory: 4Gi
+ requests:
+ cpu: "1"
+ memory: 2Gi
+ ```
+
+ ### Support & Documentation
+
+ - **Documentation**: https://github.com/terrastackai/geospatial-studio/tree/main/operators
+ - **Issues**: https://github.com/terrastackai/geospatial-studio/issues
+ - **Email**: geostudio@ibm.com
+
+ ### License
+
+ Apache License 2.0
+ displayName: GeoStudio Operator
+ icon:
+ - base64data: "iVBORw0KGgoAAAANSUhEUgAAAQAAAAAnCAYAAADgrJZcAAABGmlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGBSSCwoyGESYGDIzSspCnJ3UoiIjFJgf8nAzcDBwMDAy8CemFxc4BgQ4APkMcBoVPDtGgMjiL6sCzILUx4v4EpJLU4G0n+AODu5oKiEgYExA8hWLi8pALF7gGyRpGwwewGIXQR0IJC9BcROh7BPgNVA2HfAakKCnIHsD0A2XxKYzQSyiy8dwhYAsaH2goCgY0p+UqoCyPcahpaWFpok+oEgKEmtKAHRzvkFlUWZ6RklCo7AkEpV8MxL1tNRMDIwMmVgAIU7RPXnQHB4MoqdQYghAEJsjgQDg/9SBgaWPwgxk14GhgU6DAz8UxFiaoYMDAL6DAz75iSXFpVBjWFkMmZgIMQHAL7NSicB6KPMAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAEAoAMABAAAAAEAAAAnAAAAAPyfGMIAAAGeaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjM1NDE8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NTM3PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CnxOLvUAAEAASURBVHgB7b0HeFzXeef9Yiow6L0Qhb2LpBolkeqyerFky7bWjmw5jttu4t1kHSd+HMeOd51NWSfx4zjrxPk27nIsxZIpyeq9ssikKIq9giAAorfBDAYzg/39z52LRpCmS2Ln+XLJwb1z7unn7e97zuTd99Tmk3mWZ/qvK0/3gHvMJfFl8t2sfC6zCqmcnymXXd8ns+feqd5cvoCSXDKt5167B55P6U9ewMua65d655XJlc2Vn94Hl2FiwrXhV69yZqSd6co79f3M8tMKT77IetVOfp+Wh/amdXvqxRzteC9Pbd+lT9Z9mvfTxzWjwan8k1VM9YKnqfczkt2XM73T1PL+lEqZi9Nck+t8mvdestfmjLy5dZy72Nx9BGQskxy3wdYuhkifchUGApqcCb7SexXlTyDoDULfMxmXaAHeBwDSCRJVVKkqo+dsRj2Z8MrrkUvprs6sX59XNhzKs3SavOSZ8N95zVlRccgSo2lLj09YKBiw8XTG8gsCNjbmtZlVOdqP5Adoc8KiBWbRwqCNDeYapNYgfUyNZy2VzlqEtuiFZWlH49A3tavLjUN3PhWV+TaeytjwcNryLvnA5ybyNCkaAJU5JPKfNVcatF67u154+TRB7p27kYcBMGNuMlWfqzPXvLLqmqseVy8L4PLk2vDaUoFcG+pXrg+uTddPvVY5753fL78+16DaVxXuy+RUeK8m072vbmq8jCTMzEtzVEKaS869c3n9fNPq0KPeCZpydw3Df9ZrdwW8BfI6R15XVa7M9LqV5BMLl67SXv7Jfnk1eun8nczv0qcQcirdNeaVytXtukq9ronp7Xi5SM/1zeuoS51K01e/ztl15CpQ3ZP1TuX32vPLTk8/NU2dc2Nwr3Lv6b+rd0Ya9QBTya5+O/Dd5yxiRQ6ZR8eGLJlIOwQOhgOWTmUtFPJgNg0CCXayWd0pLnjWqEGmIIiVyXjvQxGzSFTLKwQ3EFcILgJjVhgLu/rD4aCjOSpbWxKyzsFxMkJcRACoNgqSNy0LWmlZxA7vHrWSyqANnwzY4FDKSspC1t+d1kRYYnjcCktDVlGfZ+FontUuCFpymHG1ha2A+tRmFsTojicAzzzLH6Mf3Pu4j/tERBXRzwxEawJCmEf+BUuKramh2J59ps1yNMObTMGCEM2baN1yX1SJEqncXRo834X0gVCQV0xQetwyoyOWSSUsm4ay8C4EyYoUxiwSi1kgEqZ8lv/0RDPsV6cqveZzdXPLNaMEAbm++h9lzhNV1j+6p6r0R4vn1ZvLqdv0emd/d6+nkMOrN9eQXzCHxLpNXtOe3Vy5OSGR/+67Mua64JVR3/Qyl67EXB1Kmrym9W9GusuQKzDjWWnTCqlSP8lvwOU/tTaXPO2Pn8O/e8Rr8tu0nGf36Hrr/syc35mlXYZcl71n733uWaB3ust1TX+UN9fPWd11MiPIXBgptZbq9SB6xA62breTRzosHIgCn4LZjI2lxrxWJgTHqjLX8GR9uQfeBwIhOHAa5CUbQ8ukhNDUQzsFhRFHUCLRIPAPRwcvMmBd32gGYkIdVBsKO5JpC1cErXlt0BJDGcdXahtDNgExaj+ONJCYsHC+WTKecZy/pMJs+fqotb2VtpLioKX7gjY0Ombli0KW6s+zEtpOQMQMmtFcGraunpS1JTIWyxe+mZMqhPQOJ4HDAASs7eiIjQ6nLKI+TSKNxulhkzch/sQq2f/kHgJC/CAdiA9ad9sR6z9+yAZPHLPkyJBlxlNeeRoXcYgWFltBeYVVNi+06uUrrGxeswXCYagwRMKvV/fcF4fHuXSvotl/yejyTi/t5Tk1ZSrdPSmDrhyMeY16SUrz+5BL8W6UccWExLqUSY9899Jd6r/yH9dSrg09Tw7gLNr1855F1l9Slql5md7v3PzOamMq78wXXslT+z6rRgrl1mFmcffNLRPAn59faGFgbiIdsFhBkc1vWeg4/eDgkLW2HpsqOb25ac8eA0AUpw6PtfOS/xlepBGlHT7QzhhMsDAasXEQXyJ2aZWYFJy9N2MTSBDBUMjGkUAG+yesvz1oAyeQBiIRe/OltI30jVntvJCVlEStp2vcJgoDVlaeZ6UVYTu4FSIC4o6NmpU1mA1BBDrbeUYqiNCHBCpAWUXI9ndPWBL+GoKtS2qRVJBBOoHtuj5moFoiRlIP+iAU4q4SJM7+AviDTEJioMdO7NxsHbu3W2p0mDnJWu2KNdZ03iUWLS5xecSRh3s6bfejD9D5iLX+5FXb//wTVt483xZtvNLqV6+BQLAonkJ19n34RXJOW9Sfu5pfRh0/d+P/UfBsZ8AnFMNDcduzZy/F8iyeHLEhkH7Xrl2OAEgv9kR+T4L09P0c6eGd7AX6OLUSeFbeMMwPDdt1QzmFaOEIenkihT6OSE6evLwssD1h+aVZi6ECDA+myWeWX4iOX4jaAQ6N9KetYVnYdr+StnFsFfMWRW0U4lBdF7IiSRUZuDvcPz6WZ2OgcCgAGofHraAEW8FYxqqqIvRtwoaHUD2KhPQT1lBEXyoK7a3WOD3LGgKOk6CduoKAExRBgFhEIyHLauzMyVkTgLw89BooSOu25+3oluc0nzZvzXqLlVXaW4/+wFrOv8wqFywhD5wdahhAD8ovr7S8xx60ZdfcZJULl1jv4f129LWXbNv3/smqFi211bfe4QhCFsr57+f6VVOAX3X7v04rdfq50JsAcJgYjVtX9xF09ZBlAoPo3RVwRbPBwQFbvHgxHBGxGpU1GkWx5xoaGrKCggIQKkSeQfde0oMQOxKO2nDyJEjU7wiDCEYA8TuYn28TsTyLxiKWHUvbwjVIHcUFVlgRhCgEEdfHkQQw4hWa9R4ZQbQ3O7E3a6XNoFF+nhWVF1oqELY03Pl4d9hqqostGi60TBBdfmDAYhXjjCFukeGI5cXyLVSTsngojRiftdEh+jmCYa8amwai/0Qf3H1E6koQ/T5tK88J2+E9tM/YRBBcn6lXhEz2i7MiABL3xxD39z55v/Ue228tF1xu8y+6io5V2cDxw65SyUSiKWpA1GciS5Nwd0kHe598yAqY+HBBzM59z9020t1pb2663577mz+zNXe8xxZdfuUvJAlocGd1+RmnwY2Spn396dX8zAV+epX/f86hufeX5Zc9DzJqC0FlFc/ClSWOx0fifBfHDNnx48cdggtmheDi9tLbfc6v/vjP3j2EPQuVQyowEkEAxI8uaLZgRQnIhh4vexCcu3U0z8qrllt/ugBikcEw2Ef9WOu7U3b4hR0WmBiwaH7Q9nxD9jKk6giqQ121xcoLQGaMii3UmV+O1JC1ozt3oWp3IsLnW9/+Esvug3owY8n4EMZICNwghAe1oW14wkZ74pZOMqPZAmcn0xhPtIH8qKv6J/Ff6CkDpwySUp5+KgGQHj/a3207H/qmZcaSdu6dH7bqhSuoKIMRJOUm7nQLF8ovsMZ1652BMDHQZ527d9qSK6+12uWrraJlgb31yAO2/QffseTQgK2+5XbqnG40Ol2tuXRBDYug66yBSBnnuFTLaV6dmntGRq/92ZkCcJWszK6qdXYWJQGYAsYJl2da6dl59cpP8+9+9tzY/a/unssjnVQLLb1zsvyMjD//F8FDVqzkV3UxRje/Z9EHLZVAaiyZRIw2K0anFsxo6oTo4uz+JQSfTgTGx8cdcZCEIBVAd+VR2RDWs7GRccsWVdh4tAZigBQB980LJB1K5QVi6PpyxSHZqgMgW3qUPgwnLYbrLxSNQYBoGQ49nuR7xHf1yfo/bsm+Ad6lGCceizFZ+DGwJyFefaOWX5LBeIjUUDcPKboRAjDKOveCi1JHRuhfGjyNkyfJwJEQ4uAoYr/zauTQS12S5yIIcTwjAchDjBgbHrCdP/onB0jnvefjVlRVC0Kn3KR4rj5/Cmfe5RUIQwDWvuN9zgZwcu9O2/Ktv3cLkGFyZQhc9673OgPhrk3/4hZ11S1vd8bBmTWd+i0YjjiJIUs9IUQ3qRuycv7Ui8VzlyDj571Ux4zy0FEBlUMMT5XpOdxqJbWVjB/RcGZmh/yJgWEAKG7lTbUg6lRlbj6p3+umly5X0qwGz9hzEZeBE93ODlNUVTqj/jMWnOslTYuD6hKxEuL3HG6HeNeyXl76XMX+tdIc4mJc68a3X7Wg2s3lmeZGPZSfvLi42HH8dKDf+dsdaWbeI9imhNhCdnFLrYWIwvDw8AzGpvUN815qsBAzhQ4u2l000W+xIhCpstrincdtbKiH+SqwgqoaJGb0cDwAMnFNQAh6Dx7AXpax8nkFFiyoBn6RSMJDtDNuaVx2Bi3iEUQfJe9B95xOokJEaItYgeEebAplafqPoQ+bQuWiFghS2On4o0OeyB9EUpd9IIQ2k49dIA+7QACVXHyAnnhjp40x1JRwBO7PGp6WAGjQWTq++/F/dhz8vHd91ArLq52VX0B2pkuGwsMvP42t4EWARkiB8QF3iwatSRYFRz1jQgO28oZbmKA00sCPrLiuzlrWX5IrM3cLQRDt6Kuv2t4nnrB4T48V1dRgS7jJmi+8gLpzJE5IRP9nI5/6ISA6JZ3E6Ujnt6w63ItpSOrqYPxSd1STxpAcGsYT0m715yxj4cbt9e/+yM6/6xarWjLfIY7rixqgnmAYseyNvXZ08xt2w2c/xuJDumknkxpHJfoW6lE/dUwRsw0fvRNby2LWwE9zFflddHdXv6MTuJ9wt+584AUIUIWd95+upX7PzeXWbMY4vCq8cfvV+fPjfRfy9x096ZChZnEDkuCwvfL1x+36T78bAxcKLfW5OSL7dELmSrupk8ipjp16+eVUx+zLvZPXZfor6hPRGe6K2xN/8bC9+8t3o1LKKj+79NR3Va21TjIHQeYvCGKMM7cS1YXoIyMjrn/e/HkVqb8iCiIIvvgvyWA0kSAdbk19QuSKmgIL17aYxcCJMenfiNXcJ7IhOKuMbVJ/QTBZ7+NZLPggptQRPBGBDHlTzA3EPZNSzAzoyd3IP56QrS2DPSGAfo+XgCEqZiFcALxR9+gQtoSiCJ9CpIcxKyootooltXD8QevCZgF6QBSAFYaThYGnaU8qQDoF8aBO4R36EGsqtQhpZmq6Zj5J7z+y5SkbaD9i577jI+j7NR53ngkxMwv53wDoJObJVHwY/f5aJiRMwwyc3hWUlln3gb12cu8u0vJs/sWX2qpb7rDB9hP2k+9/B0PiIiusqmQAOWT26+QuqeH469vsha98xSF+/erV1oFF9+m/+JLd8qf/w2qWL3WLlkZVGWfRC0pLqEYkWMEcYSYsiY6UBHhJZ5KVHmChE1iGBVz5xRhehGi5dHFp9TG/KMaCIrWIy0P6hfBR8uq7rr5jJ0C6x/FsLHX64TWf+oirTwRPxC5J/aH8iEUKPEOTykwigL5wCRja3zwAF4lb3erFrg9KFxDKXz0potJPEQsRDfVN/U4OxgEQuJmgRZfWKLdOei+gTlJvflHBlLTkxqj+AwjUH4ri1kVMTWORLsAopTLq+8EX3nRjrl3SAAwU281fuBuA9MYtYjaKBSrEPVIYZY48wHM+dp7TY9RVytzNEtdVPoUvezyZgqmoLdmMPATUOBNDowB41ImoWUTVAAggMTapgBcnhnuD01+vFA9zXALyKCpzw2LE+WjWhvuImMuDcAFagyBMc3OzxYhRGR0ddUjvGwO7u7udFKC+1Nc3gLisO2sZIQIonuyxijpiW+CgpU0tlqmtohMpIvjmWc8h4CxCPwf7LZWQ2xHuLd8dDcooGMW4aFjz5QIPBMNIJ2EXh1DWVGyV85c5YtB96CDv0pYaRpwP9CH+C28KQVbsBOUxVIB8R2DEgjA7WFl9vdU0NtrJw0esfd8e6p7IERyEChA/gMQgd2VAbkGQX6pAirUJ4wnALjk3AZAFcQQXXuu2Z7HuX4HIt8RxNsdF5pjo0yVFiAFYetWNEIIRgAEKio9UxsAjrzxv+5953KkG7Tt32MaPf8LOffd77Ykv/rG99fCDdvFvfoSBnEoAhFB7Hn/cidbXfvoPrWrxIhtoO24ncfOUYzgRUh3bstX2PPo4Aw4BsGV20Qfvdsh69NUt9tZDjzmkKaqutIs/dLdDyi3f/D6W2WMOwRduXG9rbr/Rtn33X2y4swtAHIbrDdjKG6+2FTdeZd37D6PG3OdwS4Rg40ffRx1R2/yN++FMPfbiV79lGz7yn+zlv78X1ed6jDolPH/fEQAB+5rbr7ElV10093QBzep/1aImu+Mv/7tDCh+8N39zE8anfFv//luQrHbYgWe32hWfuMte+OoPWGD8zO09Dnk3fvjt1nzBilz9HnEYaOuzl7724KSkseE3b4ZQzccT85QNdfbZYEePnXPrBtfeGz98CUKTZwUg+jW/dyfEdr/tfmyrI3QC4FU3XWjPfvlBu+q/vt1JMs//7Sbrb+12yHHOzRfaqpsvQLLZh4F3MwQlbL1Hu2zBJcvs4g9c5YiIOiaiuf/ZXfbGg1tc3wvKYnbN794MAcm3p7/0EO+ROlp7HAe9/g9ugyFUW9eBDnv6r37sCE9pfZmbFqbqp16amxCIKiSIIhKn4NLxHnzsIIS4vKz8kgKE+Pqu9DEYRwrblpBfaSMjw+6utRljDUMF6N9Y7uvyQ1Y6RPxLYYHFaxayPkUW7T2Jage6E2IbLsBGACdOoM9PpJEoeB8mKC6dRmdXsBHYl0Uwly1iPBW1/ArUgnGYTY+8DEjLg2JQ9DmZ52IK5DnIJzYghMsxJUkjD6IdlnyDiJ/G9hCQaoJ0A1sf6Q9YNJuwskIMnhAhD2+RLgg2EgGQ7s9Q3UcCwSmXqOzxHS+yiAX49i+nEl/8PCWrm6hTU3MpaoXrzU3ft2f+6gv29J9/zo5tBshAznwkgSs+8SmHeDvu+x5cvxoku5X3rwJUR1m0mcKJFmAcMWyos9MKKyutuLbWjm973Q4885yN9vVZ1779IGu/7bjvh3bJhz9oN/7JH7kBvvXwY5boH7TXv3e/rb/nvXbdZz8Flw7azh8+DJJ3W+vW7Xbtp3/Xrv7kf7HSefWOowye6KDshF3z+//ZLv3YB+yNf3nEhjpOOiRac/sNdsv/+kOrX7UMA+bDVtpQa+fcfq1VLWy2iz/4bvodtH4kAon2EtGazltlt3zx9+zCu2+z17//SI575+Zn1k3I0XO4zb73oc/Z937r8/bkn/2T68eK6zfa/qe3OMTf+u1HbPm1FztRv2vfMYhznd38Pz6KEfVSe/kfHiCu3FO5VLXmTMgvFeLWP/0o6tZF9sLfPeA4spB/oK3LNnzoJmtYs9Ah3FX/7Z12x5c+bsmBEdv39HaQd6X7LL5sta29fYPj2L2HZZEO2o4HXkGqGLXb/tcH7Orfvd223fs86kKX4+xC/Ms+doPd8Jl3QbC3004vZXKgxry6uf29W+1dX/4gCJO0fc++5SSQzn3tEMBau/Ov3o+OX2Ov3/eaQ/rnv/oksSNL7Z3/+702f/1CEBm16WwuiEQQhPEi4eB2SB5Cdt/YlwCeRACSSIVxdHZ9H8Dt5hMIzZ8QVGUcoiLWy4+/cGHQqqoLLJ2N27LkTluRxbKPETAULsUmELPCEmwOSEVCRiwNWPTh/rgICe6FeYzxTnmRmgoK4eoQjpiC6kaYiyFgPIU6ITUFNYB6wqWlwFglEkKTRUrqLFpab6VIyZGiUghVwHpOtNnme79nOzY9bPGOLmCv08YhWiEkBkmKCgQKI0EpwEj9URCQ0FLp2gMxE8vooiYrMQRCHXgDd99VUDZcD5gYNRmTFxU4kV4UEoqlGjVpMy/cEv29AOVfw6FarayxhU8ziPh/6XyxK1PaIEvmYuroRlQaw7V4Cbr9j+3QSy+AMB+wvGlBQn7t0tb17BCttdX2Pv4EgDhkK2+6kcVGpGJBD7/0Kp9XmNQ4QDmKynEIblxqNcuIU0CKWHTZBvpxn615x82Iu6WoFF+36iULUFcucUPQ2GpXLGXsMTwWi1E3qqjjCMhwvh18/lXb+u0fovOfyFFXqC2UXeJwfkkRCCjrLSIy81FcU2kDZcVw24foF9QY0cvT5afN5fRJY2AaQ+m8GlInrKi6HBsC/uKGKiSLd9rDn/lb2/iRd1jT+SsAlCRcJmp1Kxc6YFq4cY1tv+8pbAgDbm1kHUogng9iELzid+50hHrBJavoy5MQs16Xp2X9csrPR+xP2bxzFiLuv8FnJxGdCSdRqP4wqovUiyjqQwI1QkRKa922/RBE50LmKGo1qAdlTVXWvusYc1BAbEcVc1bqVImi6lJnqS5vqnTrJjBpXLfADjy3i7Z2s3YJG0cd0BUmfLVuWYNrr3Fdix18cQ9BZ6NIV0PEkqx2c1y/shFpASOwDxCu5Nx/BLLieGMgQmKUz8iEFRZWu8wS+2thIjIQ5mOsFeIXFqJXQ0AF6+3t7U4NUB7ZA0Q0QljXInm9Fsqg1gGb5Yj0xcBIUdsxjOV91rTuWhuIlBBok7HDO7ajsvRiBypD8sVgyb/eQ4eA8xELV1dZITaaTJL1Re+3IBtzaG8MxE1jxEtia5HaUIpUW0je4lLcjSVl6PFICLj/qoaPWXmqy7ohCG92TVgr8z46OAbhwUZBdRFCjmP0IZUA+XEVEsFMbXKD4mJ04j9GQ/CcIc1BAADevmP7HKLULFnrDHSzp1e6eBpr5f7nHrLW119ETFsKV/Ysm8orJCtrXGCN567nmUmvqIRTLbIFGy5HJyxnQTucLUDERnk14QIqRRE2n7+eOrcgLr8TQJDPM3ex4GEmvKS+Dl3rEFJCK7aDm61ifos9+rk/cYAptJJxZN66NU5q0T1WUQ5VbHULSEMO8NWmnqMs/g2f/SRRjbutfcebSChfsdv+/LPunRBQEyZAU/4w3oZXvv49J4Usu+YyV39/64nJvlMIqjSF2ELkfU+9DIF6kdiHmyw1Mkobe11+jUhjnkFUSZNaIc/ArV/8BO3KYEo0V85IKOOgVAqJ7J6LUbVwUQ/Y7fopDPPEPa/v7pnXmlsvnfzuGSghXfq1CIzUkx//yTdt4YZVNm/tQju5r9XrG3kdYdew9PEvnt3csLaTbfA86THgWes+Abdx12RZABBkfPiz9zrVoHHdfNoiplVjyF1S/dSmyrv58d/5Y1DWXLV+mdPdZUZSzDu1Ob1ZnLAfC79/nThxwj1K1BeHn35JBZChUPYAPevDCK25IWKFoXyLlGWsDANcFvXiWFfSguMjVrT1PsusutYmmhZ484KoL6OfYEGolobOaf9AJoVHATU7gJ9fSB3EHjCewd4UkldLIjqGRsR5bTqKQHBHO+MWQ+hZGB20pdEOdPgeq5RNqSZkB9sLkAhKrQqJoaeNPQAl4BuSRkFg3OLUrShFXZrPVBJDICqRbCPyFGheZkoAbnKJJIIAlNQ2gqQVLIaQxdXh/og79hzeg4j4IFRv0JZdfRt656UOAd2iAVtSGepXr0O0PBeu51m5BXiicAsvvZLBk4nGxS2nrNvqZJYy67APPInU0GY1S7GqT0oBAraALb/ueqzlf22P/88vwnmW0Ndjrl+ydlctWijIpF8jjtvvQiyqW7WCz3IMjPfb0Vc2k2eB7XnsKbwN58Ed23l+GhH+Jow0RRjhdru+C6jbtu+CgK2G2ByzeG8/lHyB7XzwMbwNa5EW5mNr2O7sA2PxUZrE6Imhb7AN9QSu7Yxh9Gr4ZA82jxJrwDtw9NXtOZvCkAPs0f4huNswRA3RjH/uPwvTte+o3fvhz+sr14Rd/jt3OeB7c9Nzdsdf/549+6XvYCd5CeJ3KYCUJrJyF4ShGMlpM20VWTHtC4mG4fICvOolTfbmj160dXdeyby+jqrliZQSD52dBQSTsS7eO0Q/FyJtVDB/cF0kCd/fP9I1gGQx6Prtj23hhpV4brZa3Yom3I49rFcfnH0hc4g9Bb3TvzxruDcaIXQGwB7pGbbGtZIIKzEWi8MPujIuL3CiS7Agy3VBaQHib5W9/oPNtv59G+zQy6h6SAU+XfDbmesug5iAXVtyZQQbBxeE7LrE6RXx5+v+vlqgd3pOQhRlPVc76rekBHkPZOEnkM7KMiVwcBC0MGTlpUQAToQhNsTzH3jWDmbGiAcKw2CAcQJzAiGs85Fy9HwiYzN4CUDCRNdhjHUDiPtsmCOaFkMBzAvpIUYEYVUJuFFoJTXgX/eQLQ912cpsHxsc86yCoKOeTKH1sGaNrGEdtGUM43eyb8Tyk0PsJsxaMXE1NfT72DgGY4ynKXAiQQyCRH6pACEIoSPsoGGwad2Vn/corRiYLM4pO7r1KTj2cvTaFY7b+JMgS3r3wV0gwjfpXAOGrnsQkde4ygTEqsfPK6SI93bBNf/GDr3wJCL5Mxj/nrNDLz7D96e5P404/TT6YStcuhKOcKmrJ4Sh8NBLz1tJXT3Au9QBgl8vKwPQNPKZ51yAowP9BBZdiahdQ39brH7NakTRRtv9yKMg3GYWJwbB2YBYiqukpQmAfRyE2QpAtSD+3+pEyq59B20vRODknv1EJd7kVIGjr21zSHd8207rgChc8N47IDYL6FO17XvyRewU2612pWcYlaGxomUeKsJR62/rQPpYAeL3QoAWMDeL7MSO3dgpXnOGyLJ5tU6CmLd2OUFRB52EUzG/wXFhLcgQNokCuLzUCc2DPo3nLscYt9vp/Q3nLMLYWUf7b4Ksi51BML84hp79KvMxiL3iDkcMQlh427bvQzJrsMWXr2MudkEgtgBwCbv0o7eDVIX0sZ95o+/N1bQDsBbl2xsPvIhX5agtvmKNM+7Vr2yhviIksgOOg1QvmWdDJwesEZtB3cpmEHmQMq/Cxdvs4nuusdpl8yBq+L9BunlrWtzaDXX0Yy9pos0Y64skhS4sg+L2f3nNEYslV6xCneohTyMENW4N3JU3SYCLYKlhTbPVr5iHyrDb9j69y70rn1eOpAiXzdkVlG/2JfjLoFoM7j0CAYATQ5PwauNGEzf2kFxILfHewT9pPqcXx5Per3SliUh4hCIPNYGxLYCLIzFk4bBGG2PEJvQQclxeEbWyslLbPoihzgpgfklHJAJhbGiBEhAbVQIDnijISDf6+lFsIyFclJEy5goJN1aMusA8hWKsHXPf12/Ljr9m59QlrbiqwE7GkRD4N0CQTxLGU1tXZmORYjs6zA7B7mF6jVo4jI0GO0Mp+4UV7z/M7qB8PDxpmEKkIEgYMs8YA3WJIORtuOfznAfAQPgv3VUhv6//85cx0lXYmlvvAXnQUxAvNBnepp4XQdAf2+Uf/2P0EgJNcGm4BaAOh6iMT9RbuuIwIb8vfOVPcfVdZqX1jY6bu8mmgCujZiE6sYoKJIZzXHl17Ik//bxD/ovu+ZAjSH69flkhhpsxiI5iDjwJBRLELOq76pZkoXwuDoGJULAQGdz3ILKVApVkTRCXlPSgOVCMgYDqmb/8qpMaVt10tRNZZTX13YBOPGXxVU4cQu3QHOOV0Qeg4J0XmORZXwVAipgUkmlLqOOqMDm5tiYo6ygx/dCl9lWZm5tcmripvqt/kqy0RkwZwJWxB37/r503oGphgzcHVKP65Td2CEAe9VFr4fdBa6M8EteZDj7kUdsQHY1RcyzDmfolbi/jneqSqqD5VTlfMhChkSShNPVJHFx9VRlPTfHa9st641Jb9Ie6XVtaF9nDKevsC8yf2nbA6eaLNgVb4l5IBCEAWJKm1wdv3vx6GY2m0V3KP4YU0/vE8yRnsIyDHkTnHd9HXW6SNfap/NOfVcF0xJ8iDEFrrA/ZdZehPrJzb4yjdOpLCm0Aq33/QMKq2dbbFWu0N+L1iBi8RyqcmCD2H+v9hNXQKtx4BA8HNrVETz9ErhsRPWbFDS0gK1uKS2qccTyL6lDTUGILdj5si5twW2oPAsbBgcGk1bLZJwn3D2AolEtwNFRk9+5BbWeskQiBAyc7LQ/7hkKNBYdD/WM8I+hrkblGOWNAZwtEIAqnRgIyk+lU0i30aF+X7frxt23t238TyoQPRSSKyyEh8zbY0WrhfvyaDM2lsUhafS1+cU09d20FLsJIVMiCR+Eq13pIRx4BlABG+fWsOkRIFEqtLZP52AKS+Gmd+sHb2Ze2HLs6KO+A1rVNddyFJF79AhipH+6/S3f1qEwufUIIzLM3Jg/g5ZstqauBI8acKKg+SIxSPZO6t9+uus4l6Uf90JMuHzgFqNorrv54qk4O4Lhl4BpUM+Py1SEvPZeXHAJOB6u8kHiaR73qT0Wz5tlNpNemMvn9zEnhKuuQTYQIpHOXy+Otp/Lr8t8pv1QCv3NOTaAuEUtX93TxHuT32qOunL6v8m78uXp9YuG14v0VsutybYHUIlK6/HnTs+ZOH10aM7gDVmrtpnuk1IiXR/lmX6p/pC9jtQTtjOfLEIheTfSfLqkApVjYxdnl9pO+P90dqLLKozS9k4FQ8fyRWNrKiwME3aBCYIwM1UDAOtJWAUEIcRZAf6TRijD6ZfEAifDlBQtRqXoJBe4Dt1Kowb10WVJFHEJLG6hEycEuYCtIGzFrWnw+rsg8a8KzEIoitlNXAilDaoe2+Q6NwKxoMg1MTtCGQovno1IM4BZMjeWzDaDGwvFWw7wAnIiQyY4lT0DY4hw4IsIQpp864CSLdDDDBuCmEy6mwS/aeBNbeJ+3XY98x9a8/R464xnkpIeLe26/7x/d4s+YdMop/v/Sj30KnacCClWG4e9KxLfH0bk3QBjqHKUTQghwHKIC1EJARxAEYzy78wKY+NOvLYW17uowlxCQGrwvP+PfSfBxbQsxxwlRvs3Vouc5q53W9mRzc6VNvpz18PN1dbISv89XfOI9rn8+t52zr7lSLI2P05P1/Do8/ExT8bPMMYMT8EvkHQGBtHmnsEiAD/Fxc4G/HI+RLsG7uLwuwZ++6y7ioHQRIEUCCnFTcPZMYZkVYvSLEvZ7vIPDNY5hf5pfZPnhAstHJAe9Qa4oSI0HJV8RiCLCbBcGI8MgrmL5qQpCy53vOr1HklgKhAxgO8CxYBMHWq00LkKBKg1zzMTT1t+PsZExxRhTAomgsrLcSvsG7ZqVDbanO2EJPAgxtgmPDbO7kHPDNLc6oGSMesNQkHzcje70ItI9NWCWG9DNL5goMbGouoEAkXvw9/4D233v5ZkIMETnmqVrQOQGh7yCKP67yRLSdh/YhXrwlObRXeLGLesvdfr/4ZeeBbF+A7FXpPw0F3U5yptAhNEmZ1V+msuJwuJ+gmxHDU6TcXYy2SVqqm6nHszVhJ+mqn/Ry03q6StR6K44v7iFu/y2/SJ+ef8+mU6Cmx+90OVJXyKqUkW0hr/IFVSkGPWP44WQyC53miz4Wp9/L5eYQlU122ubUJti49bfOWGHdzJTGgtMzN/2KwlAl4iDTwjcd6QdRQo6NYi50F4ACfNR4CeYjtlQW7+NZcIWKiFCj9N62mB+HQl0eP6NJ3GZ4u+PnxzEHcfmHtZjDN09m0YlUNBQcYVFEuwXIZAoSPxAOH8CI24luwGHrDiYskaIRj/uQUXtxTgNpFVRqcw/sUJgdcBaKvIJ9EEFoP4ohCWAO1H9SxM4pH3HE8kBd2qB1k2BUGMQhyh2AQmqKR0dxtxI8pohAQiZFPwj5EoSC9C4dqOtvuk37M2Hv8Xn21a9eLXbDFTRvJgKqESTAsAJDkUc4kRCTb8kesiyufadv0GyRLqcXMq3GXA+4wuGn3PWMBnlFDkViCWBqM1B/KYpdJ1C3HzFdbXM71Td0/sw4xnYla7ezhZLiezNF67LieYzcp35i4N/DwlESBz3PVucIJ+bL3EVqDpTYm/88En6cQ7uzSqIwFmMYY7eCUAlwvcc63A+/eLaMucNcPYJt0Yi6mdft8a1+9EtNtTeiwH0atv3/E6MlseIRHwbgC5r+NkOeI7O/hslyQBWgsGxugndGz4BLnFoB7vp8lFx6UOG+S8rK3NEQJKAEF0BQVIRRAj0UaxAPpKvgnYkBQRBxGiU0F6i7ibgqv0Y+fIwrDXjfSnjfL9dwQXoFpQfJY4igTEUxBtmv0o2ifSQDhFf0Q8AcFIQunu4oBQ842y/CuxoAQKFiiA+0QpCjZM2P33EglJNK4ocvOhQ0VJiMUqJsVDvs6x3CmLQc5QoTNLDEOn6pQ02kg2CgxzQg35v2iEoNy+DlRShE4rkEtQBomPYD2ATBC2hwsxYD3KHdXwSxybFsQFI1K+cv9xW3XAXPv9NhMzuxU98CbrnYg/wBVw04ER46fDTuI4mWcgqBGk45zyXR5uLlMabXLPcRT106Y4RQDrnqpvf7hbNATDIPnmRRwFDW775DTvChiC1F8KSu/rWW2zdu99BeRmPZFRChGOSHJLxLcACqnrpmALuzrf2ucirRZcqLNezG4hq63IqiAx61OUZEF0WV04GS41H7cpopgjBRZdfBFFUQIdHEL1K6AH51JYQXtRfBErf+1s73GagtXe8zRkfZZn3RHjXuOO26quzB2j1eBaCq3e6u/Zz+rbaEsdS8M+Tf/4tPA77lQSHybcL3ne9XfSBG7G0HyQYa5Nd+V/vZC3r3Lhc/TLC0Tdxdy8eAJsE9TtuT9r+p7ajAh4gRPtyAnLeRI3bybqsd1Z4AY/GwqDdnP7qCIK3Zm7Qp/kzjr2hgvnIJ7J0kNDZkXiPQwqtc2tr62QpjUFp/lg0r/IQxNHV9awPo0b81p4HNnQRRjwGxy1HUipD7N6bV2S941UWJcQ3TLTg+Ch78tnkU9FY4E7xlbsvCOceHx9lHRUpSGgwIcUhLP7lGBKjeBeQu60y1W6LxzsIOc63ahB2qH/E4kQZKlR6eARjMrkKamN2cFe3lRMLMB9bVYoArcLiWhubICCNeJNsAKIC400Tnl4IwZYrVLwUbAUuaQccKyIyUMbiGQRASBCCkxdV1WHkO0ohAfsEvvPVBKgsxjvwVTdJk7N2hgcZ845zDFjbDmK+efYvAZ/rCsDmLoc43qP+ahEE5EuveRsusPPpPJwyd8nCv2vTj3B/vWyLr7yC+IML8OM/Rvjv/Zw4NB9vw3q4VgfbJktAim582vVMehiE3+PFBixfgvpS5YDXSQK4+BShV7d6ubfI9ClJrMLJ3fuJVizEx72EDnmA0bX3ENSVeAC2YRbXVhFheBykeM35+VsuWudEeBkUZc0VoSkjmk/bgofau3AT1hMd1+Ci945t2Un48Zv4zJe5iL/l122E+suYCp1g3NopKAitZ0OQNhClMfSMEAknAtLf2ol7sYWgK6Izc8Y4EaKdDz7nkH8jLr7y5jrb/eNXWMNSpLg4fdyOS3A/cQu7XZxAvGeAMY/ixluA8SkOUWd7b3MN81KGP34A6egwbtYq17Yi87T+a2/fiHuuBeNouSMaCu3teOsYhCZCVOJi3Et4W+j7r9slA2yRtszCQbNw3n4iHHWun2yLivkvKcHlCsOQsU8nAkkV0HfBoC5JAcqntBgGOoXSMt0Wx6c+BDKWsSGsEgmhb2DcTtoCjuxigw+EpryiySrZsjAwMkhcSDF6PoXYuaNzCRJY/gPAlAzdQfbsFhHkf9H8xQQC4VkYPm7FB1l/du0l2MevXgyxi1BifEcHpxMBF2vWzrPDh3EXQ3jya4otjyjBAqT29ADjQpVME2iUZbdhtLLK8rrZIwLnFwGQxV+uQElG8jbFOGpcODCFmRqxCCqDVwzAwRcforO9HPmFaMpESsTXpW29jov/lPVWnoG2Y3CdxfjGL3AAIp0jjwnS/I52HHdIF2tsFl2iZj6ka3//geeedgE+Tedf6DQN1zAv5a5r3boVQC5DHH0/4lOZFdVW26bf/0N841uwN1yA2/Hv3G4/1Xfpf/mobfv2vRCzDpCmwnbc/6Bd90efpI0IMQmbnRjYc/CIVbzWaJf99m+hVuC2/PLfQ0xaQIYeAoe22qX/+R72DTxix7buYF4aiSV42q78b79FP3ZavLvXDr+41e0DGDjRSbDRJlyaZfRjLSclVSDeP447c769fu9DbD66EyJaTxzCDohTH2PcApe+hcCeb7Bh6Xb8vo3savwnjzjBmRX4c+2nP4Rffcge+aOvMofL0CuTbEZ6xG75nx9jPFB5QTJzpp11/hwpbPjaP7zbEYAj+P933P+ce/Xq//djR5R2P/oqROag/eYPvgChO2YP/dE/2nWffi/xAmvswU/9AwShwxEDcQkRcQHL3qd+AqHdakuvXsuxbh226TPfgDBhIccir7De2774fqRGhedqHX+2SyUE6P8aVxgYjMh+wQfF0UkBSW2YkaQJMsfZ/SiuKPFeabIL5BFDP/3SmBwhkFWfPONEEyUS41ajMwYwsA2NpizGbr0JiDWbA+DuSFWcOizio3JZnc7DpiAhorYKZ1ERqAanCe8gTBnFCEAciNyyiYPPUgXzyiEhCO42isBPJIFF1Tci+5bDSCIQNFqxlgWovcB4kNOKx1Ehzl1xno3T5o7AXusnBB4ss9Sxw65PRfQTxQXcxSvhiBsGQbwBI4xjJgEgk0TXiuZlIPkj7AfYaQsuJrTRie5BEGCJtb3xmrvPW3cxSD1zsqZPnJ5FBGTYKOb0Evm8GZOliR4cfGu7jbUfF4klhJIjjs8518IE66hz2l0X4xThydOFc5Wq39pckySUs4C8ol5p4uEjHDOm51GCJsSF0hColTffgHRwHpx/N0Eq++22P/sCrkyOTdq+E2DA6Ib4W714AYj8McfJH/38n4FECaSLxwg8WWnrP3AXxG/QfvTJzxMJeBSE2W2LLl1PxOD1tEOsPRR57TtvtFaiAS/+0HvYldjiIgajRBPe8Me/4/oT7+mzGz73O+jiFcTfb3LBQDd+/rcJnrqWIKjXie1/N2Ii4j9AIy5+4PmtbqQ3fu7jjss+9oWvEbzzqi3cuNapMJf81u0OqR/85N+4IJ8VN2xwHgvFCay6aQPBQnvYefiA+xQTZ77hw7cRPHQhOvy1tuXbj9n1n3k/wVarICwveQTcLZCHemr/OOK+kP+iD1xn6955mT34+38P4ezzZh8s9V1yW77zDH3O2N3f/O+0ecCe/Iv7YRZvuX0Bzn2YW69f9U0jS0HEShCdhZugm1WXh+2GqxpAsgxuvLjVs08jglguDsu+Jms/KfuG13PBoj6++K9n7eKrLB0nzj5uE0TcDXJ+fzX6fBkhHiW49/rztW2cSMd4PwRA28a9nYaFxQQIIW2IgJQRBBVEFWkmjLg0guiPlFXOGYPx/Y+zIWnQqudVEFMQR0QPW/sREBmrH+eCWCMRngEccR1tPZZGyhyE+BZDVAyXZEVJxBLUn8qy27aozU70jFhZUbUtvaTKTmzrc94BZwfRePgoJFp9GRkhrmL2QslwV0DwT+3StcTIv8ImkYuh7hJ/srbkiltBMg7vePT7FJtAROfwDhmzznCpPqdKcM+g73Y9/7iNs186wJ6APIjC4J5dNtbbbQ3X3eK2C4sAOX16Vp1aGCeyYKSRP1WXdvV59eNewRagRVKwUnGNt+FjsKMTYlLmOL42zzQQKej0acYSLSpyCBSG4IQR4zIYUrQFeKAtbU//5d+6+qUGqNy6d91iW791Hx6OLY7bn/ue25whxnEHFlz91bO2GWv/goAgMThs277zoOQaQo5Pwn05/QeOLeRRfuVxF5Cqfvcd1Uk7RAVqvnhVuWAeUlC7IwAKF5bOLf95jFBQb3+AV1wBQbUrFthdX/u0dRL11sdusG3ffcxe+j8PsF7nYm3Gp8Slvfza2OM6lGvT2Sd4p7tCgXU1rF5AX6sQ9ytciK9L9OgE7abZSNTnVBDlSQx4brSBE2wuYgz/9pfazGHsrMaVKuOXRO2KCmxQmr/sdmte3GkDbAzKjMLBgR/+YyCcsB4ChWLFC8lb5Ti35z+ndta1v7+PNElEYVx9BNxw9FfHKOI9nqooDEVdiKUR0cMEuzEPQwkIQAp3HBJbWlZ/jHsVbIoS61cYrvb2N+NKrItyluBYp/UdfBLDXLcNEKhUISZGfT2c71fMyVc6XTiC1FxbVQaMdEAwovwoCEZf7AIFRG+Ow8n7xfwKURMLyuhbwK5YvsrWEkmYzBbbpvZvWfFQDKLEqULsvJQqEMR2MThEKLh+kWjWvLmvAtDGdZfBPXe4WACH+IhHkENb/rZ3stgBiMA/M24o2fkbAeozSwJ+G5rM2KLl6Cu9ljjZjmQBFVu8zMLoKwL8M19MBIRI+wMOv/wSovRmx+UPPPu8QyjF/DvVhMnzEVIHgshTANlzBGIEnUjhwQzALazaU590CQmE8LXLFtmqW6936ka8t9d5GajQbv7iHzikfvZ/f41Q5s3sPrzKlZMbz7XLN7VLhS79Fc4BWHbtRltxw2Vw3ac4S+Coy6f2lF+2CZ0epEslCkHsAQiFjoxGeiMsdog0gEaX6nT1MuO6T0M2ReEd37aHsxt2E8p8Jf1vcfsChjp7nUTk+kQVzoUHAVKEnBBZh3+M9nkbY1RlPq4sXf3H2dIb5x3IPRupJUqLkPQe6cSOwFZa9gfoKiJ23R+3S/gZ/vyrkQ3GhHnTCgvqrWkh5w3ko3Ozo2bbnu020V9ujahqPcTPJ1jbMgJ65F9XhN7wMO4zSaaiDKyMVASd7efUBCzobAWEYWCRH4GxQE/LiMxLcdbFQED2GoxqBPdMZHVohzb2sCUcAh0GXwq0JQ+xX6rIxASeAhjL8MmdNtS6jR8qQdPNY3NPNG0n2gecfp+gXB6bZlMg6nmLmznMFOMeBKeoLN8Gcf3FIGghTkRKwgzTLGAFxCITLLDrGmst0tcGIVsDAbjAFqx61DY/0uWIisxGCibSz4h5cMj3udZKBqeiynq2A19tRzY/AWdaBkciLl/cHuBbfu07HBDv5jhwLXzLhZfNVc0caVqVoBUtXmHRCrZIInqGcfelODD0bABI/Vr99tuIN9hvL/3d3zHZnjRQt3KlLb7qSqgum4tAKg1OBpHaFcudyP/qP3yDPQLziON/xi7/7Y+4hfWNaMI+Z7yDwq+4/mp79evfclKEVIruA4ftbX/w2/bKP97rELZhzQonfRSxzdfHwd2PPoel/GY3Vme550mII059cs9BCA6+4Tf3wzm7EKm7sPbGQKA2+vIK3P1cJwlIdF5y5Xp79Av/xzZ/axNSSpCY/AN23Wc+DLJ6+7q9yQQYGZf0cx9xRExaEf9f//6T9pMfPOWkJAUwrb/7BqQcLMzsLtT12Be+YW//849B2JfgQXnL7v3IX7ox6Z0kigUXr3RE6LmvPGhvPvQq+xL6J8coKcO1CZKce+el9vAff9u+fc+XEHFHgY0atlevIjIN6Pp1upggnXzbA5FrYiMNLJZ4/QGi5IgyZYdeO2dEROCEshNIpYuCfEubT2C4I54eQleHcS1PqmWcA0KAp2H05RIs6pwHQkgxP+HFbr4shjgZCRP48cfLz7VaAoOkhkoy1dmB2tob4lCcC+fNtw2EwmuG0qkR62jdbYnunejqXdbZNWJ1GFeDIDNKsHX2jlgp0kKA7/GRrC1rasQOjTqBnUc2h57eURtFly/GdpAFmQuRSsbC1RDwGseADh9+3YqThziXsMxiLes5gqAe92YbQUAYCgH2NFQgyJhlEBSRm7EZSIDrqD6DFHcvrW+xwc5jAOMWCMBygNc7SkvcsnrxSijeKPrfY+jXbF6YvxQAb7Wu/W+xr/9y8haxs20XImgJVuVmh+DaGTiODSAAtQopwAKDH6SVTRXEK3NSkPRzibrdB/eDxPzYyPKVDEohs+oXs8dC6Eix5vUXYomvAambMExdzXbbO7FIo6bwvpB9BeXNTVBgAln4NJ27FuTrRMTt5cCRa5EgsK5DOMqaGth4g6mWceoIstKGOvrZwPbiJvp9wBGBtXfeTP+LGet8uF0v6kEHiHoJlu81FMuSt9FJGBLvpWqUNtSgBmgHJT/ScM5StxtQVvfVt16NHaQKg2qxlbP5J0pfNR6J/NriW4HlvhBXYsOaJUgKxxxBuuB9N1t5Y41DqWLE8TIou/qqo8iUX8eXaT48aW0pZRe7WIKG1Qud3r/yhoudylA6rxI1bhEHbDRgkGzEmLjYifCVC+rt3HddiaejeVLsbzyXd3D4hRtXc3jIBdS5AK8H84Sfu2F1C+XnsSlqHkRkoTP6zV+/zC79+I2MGbgAoPxLSzVJofxEJbkX0xL8bHOlz5GmknPWcZq8+bjkQseP2Csvb7UXXniKHXPHkdVhEniWqoDLJAExCFDOc6P1DPDDG+WlIauuxDrOfvoI4rdsSoUE6cQ46qukSMFQY44oi5EV40cvASm7YgtspGwRhAMGqTMBgW95g4bZIIQJz1agViyK4rc/ss12v/6otR3eZtW4/Ub4+a9hpDHZGRrKkCSw/EfoUBHPWPQgJqjZEIcsur4Mdkfh/PoloHmkDbDzsBK7VlUVv3sYKuc925K3/cgCyQ4bOMkRdNle7GrYn15/mV8BQlVD/Sjih0OyeBskqLpQYB5mbAZyiAZya/GE5O7nvziZcMcDX3fAtvaOD7LY9VTi+fOlT+97ZhMupucxAr0HClXACcLf5QCKz6CHc1gBJwGVQPlaLtroGQHxAoxzVmCa3xAU1XXEBqCOEJOtjUWeqB4lCOUR14bOBJg6gViLr47RL4k/TvT2Vt6zQ0i0RscCuSddh0yW2vHEarlH4GRICM6HTT1SXVSlZxiUOI6nVxZjUXAuGSIF2BqnNshoYkSQXDw6Y3HWZfJKgnD9Qrx2un1u/mRc0zVBO54OLws0QEcf6Q0Ly1l1UgV4L/uA2gnQjvrkJBlENc2J2nF7DSRA8aw6JMF480Eil9qSBVqX66NvY6Al/50nHSH2wbmUU/OhTUkaj4tboG5Z/VW/ahUojCOBKE2qhtQIIYpXn9y4nqQl6UDP3iWxmcv98dOUQLpL07Mu752Xd2Y+vfXy+un+XelTz5N1TNY77R1pRXkjNrxpqz35SIL+h+3S86O2cUOVJcMEw6ATT4A0IqDz4LglhXBc1oHZd+MeYuddUpZ7xlsANy7EC7a/tdeSwE8RzUQx/pVg3CtkXsZKF9iuyguQGPAqpMO2BCN2iOg8uQq1Xbwxud+q08etmxOr2rv4MVDiEmSZHwCpCzACdrGRqLwICSBnP5LbTgZM/b5fMS5i+fv3HOvB4p9hu2+hdbJrMkWeCwkBHkWVgG6w8W7Emtkl2cr26n62Z69kB+YoUs3LHGTz5tYBtw+iiEhAwYx+hBTUcEs0pwqgBdAlQIoWl9qa2+5BLPwme7K/ZqtuvMuqF8GZ6YyAdvnbFLQTsF0P/zNiL8Y3JsxfGLWiBp2OTB4UIMtH389ggEthB1B6PkeBKZgHcHSr7vJqQdXD01zqVxrqLYR3SEC97k5+AbnSPQBUd3SoBm4yAU6uSif+qw19uNwxWq6v9ALEkvrgrtwseXXkRNxp/VI+T5WgYuZCIqd/eWVYGZIEtNIh/Q54RjwPURxiu0weQmqDxiTwq3+qV4ini+++muElTP1VPaiwLs9MJNGcEEtOFX66az9XVBFxbhpo1Bu7xuk6DffyxiNPg5uT3HxN1pfr91Qvfo2e6Gs6AUPgvL1QGJ86sBaD8BWDTGEs69lxkIgdMxGi8WS464HY50FwYfqczI+YD3GUWlPEL/4q9HcUghCB2E+QtxjjW5ay7Rjqguysm5ffg6sOwy+BOFHE8vn5xVZGQF0e6QcPPW7RdL+N0nYC1SOOha+xrADun+RsQX42DAJbqP0K3Otwb0fLI9Z2ogsrPicwIQGMQZxfP4HhFUItwt3JcxjPhs4hGE6MucjFCOrGOBJLF987OVthzcJafiRk2DpLMLofS7hfAYpTl36CLABRUUCS9gJoOc9IALScAooYx4Gf+86P2J4n77Pt938d49vltuCiqwm48cJ1l11zmzPQ9bcdReecD8eAw4EoIYx2R16uTr46AAAJqklEQVR7Hus0AdjTEEdUVWKPLuUT5/Mgl7+ka6/0osuv0FtlmePKQWLuzcxvc2T3k/yMs6pV8qwkv8Sp9xmZ/QpPzTaZMlcWP82/+5lnf1e6n+bfJ/POTpiW18/z7+CueZ9jJL94z6k4yNFYoXKOEpc7DgI8NF5nvRg/w1i/C4gDkBjfD3MIgJgL64osARJJNx7Gt0/UPsY43GUg9BibdZIQSklBoDVEYAJbQcoqQMQCkHeCnavzqjstVbxCNkK4PycPHf+Jndz/HFRo1MZi+oEOgoBG8NgQ3gunsHzU0wzEALoNAcpYdSkWPyQwqQDleG6QOeFZ2BFQY1IwW34UDP0dtZPTlyOoHnGkzggEoR9VgKqsGvXyyIkBm99YZnkYKUeyKX4DcQBpBkmbOsWsqyAifdzlCpT6ktAJQWcz0xKxdTbg2ts/aCfe3Mx5/884D0H9yvOsbvlaop3qbPFl17GSdJoOSzqQWKydgA34+IVe8hxopSUOv/Hg9zny691w5qTteeJhO++u93sEQVzarQtbIysrPbH3lwkdZ43lZ5iVX0YdZ6j+P179cmZAyySVKAkHLcUbFOQMLOLk4KJhJwKPIpovBFmq8d2LyY0izYVAjDjPklS1XduFCSEtSCNMc6x4EqSPAMfD7KkvRwoIsw8gg6g/Ec1YfbAfyzsn+2BQPbnnaes8/Bpn9iNtoO7l6Rc/OA5c0XtN+PNL8DqcQB3oOckxYhCQcfCmmujVAfYqlKDXF3K2ZBxjJKhqx/EKJBEei+hEPtJICV6Htq5+q6kpQaLIWAcHtFRWF1o1Bt969oAkkGSSuB734lpv52fAA9gthiESYSL/TjqyIronwyfSOC7EsyIAWhJ3cATcufn8y5wBsH0nJ7rs3mHHt7+KG6gWAxghstW16Eb4Ntn/XzKvESs0gRacByCfqvYVjLNBQnq1jHM630/YLt0siqQgsV12AB2cIXFeOrKkA1+0Vx9+PS6fIv2qKIHa/1W1/euxAlO9OPNc5MHli0H45Bi78WBIoQkIgXOBERRUxZFbhDLH+dkt/UR3XMY+LPtyryWJ4Q+gNcppFwEWx+HYvLJYKSG6cE2hTSXlh1NY5PtT1gz31o+Ppgb7rH3vU9bZthMCQZgwxtMxCAIUAqSjXgKAFCEof/wJkF9HlY2CxGXE7ZdwSlMPAUDOc8GhoIq4zcC5i+D4lRCdGlzCXcSWZGGSeZQL4laMDxCVK8YKgehHvD9JcFME1XhJU5lt6SGGEImiuz1lQ73sT+CsQWmowiypdIVsDS6tREKamsyzeAIhhcD5xWXE4t/CLrbLsYwfwbq5j/PwjuAB2IluIUXU2D78HqzFF2JowthC+OJQ5wnOpHvMcXg0d0JqCZIRHNOr7fffS3oeUsE7ICTznHFK41LaL/Xyq/tF8OfMMPdL7e5/VPaLzgDbY7McuAnMygSj6LeEftUHY1hjfSkqAXo/PxcUZRddmPj7DuICKvlBEjy3ROXBORHbq2NY2PlxjyIMgRmQPEo0YAKVQO66ghibgWrzcS/yA96HDvNT3m/aaG+Pza+tcy6/ESSGQd4VQWgq4epJYgOieBMOcRxbiqjWMESiGERswvOS5HtTLW2liK9ATamW1FLAbz6wcw+MtV7UmAGsfVU12M9of4g8hQQFRak/A/Os5hi19nbOdcRDkGLrYx5bkQv5KfHjhzD69QUgNhgAYeBiqiIAwwPYP5AOtO3tDAzFw5hJfMk9uKAIDFMhzjWvWbGW8/HWgbSKYU5AuRLOolwgYx9c3+EL91LCgc+76x6nf8gyr0tI7ox+UCXhegBKrCAMn+t7rc8GAvVXHZl6S2mvndlZZ3+fHEjuBfVMJalev0DuwbXjp02V8VM8o1quHyoy1SU/i1fn7HQ/r3/3c8/+rnQ/zb+L3Lt0EhyB1AuXMJV3aiC5d/+2N9ejqT+5xv1++n2Z/n3W8+z58ouccp9ebuZLcB1Y4sc3iforRhKVVDo2Lm9AGWG57KzjxzT0E1068WcIEb0E2JPInoTDl+HPHyB6voTDOUYlOaA7p7WfGBiPYfkfIUgqACGIcTqwHD06pWew+wQdwEZQUIJuDtGh3nzc2jIkyl2nfRyjwVHrbCd8nXP/ljXW2wASxACb0RJJfg6c+iaorI8zA3RKUEVlkXVw3P0wVv5aiMfIGD9QAhWLD7BDQK5DpJt8PBcxDh6R8bIPL4DCj6mGsRGgt6jahnfqzEFUB7i/x1DpIuCjnyx3x7lxeKlOfaDffDTpDqD0oEv3KRF8+mtlcx/JFFATZdWvCUWIEwggxntILUDlvcNyISjiBvuUhdye9V6veJ72XYvm2sm1zu0UUFZ+WWKZaxbBq9c1I9EoV053r6AHIMru9WXyRa4hXkyDIZdPef00V46suss+oXSvMu+7a1F1cnk0zXv+aX9z9U4v7or46dPL+2n+3W9I/ZlRiO+Tefx3yuA9S/Sb65os4l7qm+qZmTpXudOneevg9WVmPXNVO1ea6vZK5vqsGwmTtbnv/JmdroJ+mrvjZYKLBvSrOSByIwFcLXXszuOk3jhWcLnwZOg73q6fTSPGAp2eH7Rj+AVWBMHoCqVsdGLMKvhZoQRHABdUaCcgEXlgmdyxEbhqku29CazvQdx/URgd4S8u2Eb2BNwGFs7CpcknIqIAnhK4djmuvELCu3s7+Em3cL4Vs7sz0TNkcYhFAoV/KWc8dhA/sp9fdVrUXI7vn1OEkCnysR3AH4EAfpYMKQa8JjYBj0Iqbt3jJy3SgFfKkCKwLA724oLE+4Pgj/gv7u/NjSQAzWQeNgl5jfM2Pf/6SZ/jejM8babdjHt+Uc2tAwz/de6dyrhH/aURP9k9+e9yq+xuLgNDcHfXl6kyucKus65B1ZKrhJsG7mX2yvltTCZPb8dl1GD9vN6jjxD+t+l317xLyJXzX069IGXWOz/P9HKziMFU8TOVnVHRrC85rp9LnaqPBPflTPVOvZtRTnX9DGVd07MqmJuoTLXnykz7M7nmLu10+XLps9pSES/pdOWmNeQyA/gAeyk/PNJL9Nw4mNNSjw0f4FI/tHNO1no203EKD6fogiQROPDQRAJujE1A+wXg+vJWhbCYBckrO8C4fh2Y+AHF05eAxIoQVDyFTgYuxcWmSDsdKlIEso6BuCPUXQCHjkJsUqQPI7qrzhhqQQopQTG6RRAF+f3zJYFgM4jhquzgxGVZ/cuJPgwwDoX7hujAmFzzuC7D3MdgoPKgaTtznN8VCMQmrDLAUesQok5CyXUUWGJULmjmDpgU7ssboHl0zBeD/f8DCNGXWM2TfMgAAAAASUVORK5CYII="
+ mediatype: image/png
+ install:
+ spec:
+ deployments: null
+ strategy: ""
+ installModes:
+ - supported: true
+ type: OwnNamespace
+ - supported: true
+ type: SingleNamespace
+ - supported: false
+ type: MultiNamespace
+ - supported: true
+ type: AllNamespaces
+ keywords:
+ - geospatial
+ - geostudio
+ - ai
+ - machine-learning
+ - terratorch
+ - terrakit
+ - satellite-imagery
+ - kubernetes
+ - operator
+ - ibm
+ links:
+ - name: GeoStudio Documentation
+ url: https://terrastackai.github.io/geospatial-studio
+ - name: Operator Documentation
+ url: https://github.com/terrastackai/geospatial-studio/tree/main/operators
+ - name: Source Repository
+ url: https://github.com/terrastackai/geospatial-studio
+ - name: IBM Geospatial AI
+ url: https://github.com/IBM/terratorch
+ maintainers:
+ - email: geostudio@ibm.com
+ name: IBM GeoStudio Team
+ maturity: alpha
+ minKubeVersion: 1.24.0
+ provider:
+ name: IBM
+ version: 0.0.0
diff --git a/operators/config/manifests/kustomization.yaml b/operators/config/manifests/kustomization.yaml
new file mode 100644
index 00000000..c6456450
--- /dev/null
+++ b/operators/config/manifests/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+- bases/geostudio-operator.clusterserviceversion.yaml
diff --git a/operators/config/network-policy/allow-metrics-traffic.yaml b/operators/config/network-policy/allow-metrics-traffic.yaml
new file mode 100644
index 00000000..81afc99a
--- /dev/null
+++ b/operators/config/network-policy/allow-metrics-traffic.yaml
@@ -0,0 +1,27 @@
+# This NetworkPolicy allows ingress traffic
+# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
+# namespaces are able to gather data from the metrics endpoint.
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: allow-metrics-traffic
+ namespace: system
+spec:
+ podSelector:
+ matchLabels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ policyTypes:
+ - Ingress
+ ingress:
+ # This allows ingress traffic from any namespace with the label metrics: enabled
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ metrics: enabled # Only from namespaces with this label
+ ports:
+ - port: 8443
+ protocol: TCP
diff --git a/operators/config/network-policy/kustomization.yaml b/operators/config/network-policy/kustomization.yaml
new file mode 100644
index 00000000..ec0fb5e5
--- /dev/null
+++ b/operators/config/network-policy/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+- allow-metrics-traffic.yaml
diff --git a/operators/config/prometheus/kustomization.yaml b/operators/config/prometheus/kustomization.yaml
new file mode 100644
index 00000000..251300f2
--- /dev/null
+++ b/operators/config/prometheus/kustomization.yaml
@@ -0,0 +1,4 @@
+resources:
+- monitor.yaml
+
+
diff --git a/operators/config/prometheus/monitor.yaml b/operators/config/prometheus/monitor.yaml
new file mode 100644
index 00000000..76d3246b
--- /dev/null
+++ b/operators/config/prometheus/monitor.yaml
@@ -0,0 +1,27 @@
+# Prometheus Monitor Service (Metrics)
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ labels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: controller-manager-metrics-monitor
+ namespace: system
+spec:
+ endpoints:
+ - path: /metrics
+ port: https # Ensure this is the name of the port that exposes HTTPS metrics
+ scheme: https
+ bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
+ tlsConfig:
+ # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
+ # certificate verification, exposing the system to potential man-in-the-middle attacks.
+ # For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
+ # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
+ # which securely references the certificate from the 'metrics-server-cert' secret.
+ insecureSkipVerify: true
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+ app.kubernetes.io/name: operators
diff --git a/operators/config/rbac/geostudio_admin_role.yaml b/operators/config/rbac/geostudio_admin_role.yaml
new file mode 100644
index 00000000..5c699eae
--- /dev/null
+++ b/operators/config/rbac/geostudio_admin_role.yaml
@@ -0,0 +1,27 @@
+# This rule is not used by the project operators itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over geostudio.geostudio.ibm.com.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: geostudio-admin-role
+rules:
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios
+ verbs:
+ - '*'
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios/status
+ verbs:
+ - get
diff --git a/operators/config/rbac/geostudio_editor_role.yaml b/operators/config/rbac/geostudio_editor_role.yaml
new file mode 100644
index 00000000..a21823dd
--- /dev/null
+++ b/operators/config/rbac/geostudio_editor_role.yaml
@@ -0,0 +1,33 @@
+# This rule is not used by the project operators itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants permissions to create, update, and delete resources within the geostudio.geostudio.ibm.com.
+# This role is intended for users who need to manage these resources
+# but should not control RBAC or manage permissions for others.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: geostudio-editor-role
+rules:
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios/status
+ verbs:
+ - get
diff --git a/operators/config/rbac/geostudio_viewer_role.yaml b/operators/config/rbac/geostudio_viewer_role.yaml
new file mode 100644
index 00000000..b3426cec
--- /dev/null
+++ b/operators/config/rbac/geostudio_viewer_role.yaml
@@ -0,0 +1,29 @@
+# This rule is not used by the project operators itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to geostudio.geostudio.ibm.com resources.
+# This role is intended for users who need visibility into these resources
+# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: geostudio-viewer-role
+rules:
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios/status
+ verbs:
+ - get
diff --git a/operators/config/rbac/kustomization.yaml b/operators/config/rbac/kustomization.yaml
new file mode 100644
index 00000000..e10a68e7
--- /dev/null
+++ b/operators/config/rbac/kustomization.yaml
@@ -0,0 +1,28 @@
+resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
+- role.yaml
+- role_binding.yaml
+- leader_election_role.yaml
+- leader_election_role_binding.yaml
+# The following RBAC configurations are used to protect
+# the metrics endpoint with authn/authz. These configurations
+# ensure that only authorized users and service accounts
+# can access the metrics endpoint. Comment the following
+# permissions if you want to disable this protection.
+# More info: https://book.kubebuilder.io/reference/metrics.html
+- metrics_auth_role.yaml
+- metrics_auth_role_binding.yaml
+- metrics_reader_role.yaml
+# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
+# default, aiding admins in cluster management. Those roles are
+# not used by the operators itself. You can comment the following lines
+# if you do not want those helpers be installed with your Project.
+- geostudio_admin_role.yaml
+- geostudio_editor_role.yaml
+- geostudio_viewer_role.yaml
+
diff --git a/operators/config/rbac/leader_election_role.yaml b/operators/config/rbac/leader_election_role.yaml
new file mode 100644
index 00000000..5e42ebaf
--- /dev/null
+++ b/operators/config/rbac/leader_election_role.yaml
@@ -0,0 +1,40 @@
+# permissions to do leader election.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: leader-election-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
diff --git a/operators/config/rbac/leader_election_role_binding.yaml b/operators/config/rbac/leader_election_role_binding.yaml
new file mode 100644
index 00000000..8014c866
--- /dev/null
+++ b/operators/config/rbac/leader_election_role_binding.yaml
@@ -0,0 +1,15 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: leader-election-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: leader-election-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/operators/config/rbac/metrics_auth_role.yaml b/operators/config/rbac/metrics_auth_role.yaml
new file mode 100644
index 00000000..32d2e4ec
--- /dev/null
+++ b/operators/config/rbac/metrics_auth_role.yaml
@@ -0,0 +1,17 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: metrics-auth-role
+rules:
+- apiGroups:
+ - authentication.k8s.io
+ resources:
+ - tokenreviews
+ verbs:
+ - create
+- apiGroups:
+ - authorization.k8s.io
+ resources:
+ - subjectaccessreviews
+ verbs:
+ - create
diff --git a/operators/config/rbac/metrics_auth_role_binding.yaml b/operators/config/rbac/metrics_auth_role_binding.yaml
new file mode 100644
index 00000000..e775d67f
--- /dev/null
+++ b/operators/config/rbac/metrics_auth_role_binding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: metrics-auth-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: metrics-auth-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/operators/config/rbac/metrics_reader_role.yaml b/operators/config/rbac/metrics_reader_role.yaml
new file mode 100644
index 00000000..51a75db4
--- /dev/null
+++ b/operators/config/rbac/metrics_reader_role.yaml
@@ -0,0 +1,9 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: metrics-reader
+rules:
+- nonResourceURLs:
+ - "/metrics"
+ verbs:
+ - get
diff --git a/operators/config/rbac/role.yaml b/operators/config/rbac/role.yaml
new file mode 100644
index 00000000..d8c218b5
--- /dev/null
+++ b/operators/config/rbac/role.yaml
@@ -0,0 +1,109 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: manager-role
+rules:
+##
+## Base operator rules
+##
+# We need to get namespaces so the operator can read namespaces to ensure they exist
+- apiGroups:
+ - ""
+ resources:
+ - namespaces
+ verbs:
+ - get
+# We need to manage Helm release secrets
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - "*"
+# We need to create events on CRs about things happening during reconciliation
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+ - update
+
+##
+## Rules for geostudio.geostudio.ibm.com/v1alpha1, Kind: GEOStudio
+##
+- apiGroups:
+ - geostudio.geostudio.ibm.com
+ resources:
+ - geostudios
+ - geostudios/status
+ - geostudios/finalizers
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- verbs:
+ - "*"
+ apiGroups:
+ - ""
+ resources:
+ - "serviceaccounts"
+ - "services"
+- verbs:
+ - "*"
+ apiGroups:
+ - "apps"
+ resources:
+ - "deployments"
+ - "statefulsets"
+- verbs:
+ - "*"
+ apiGroups:
+ - "batch"
+ resources:
+ - "jobs"
+ - "cronjobs"
+- verbs:
+ - "*"
+ apiGroups:
+ - "networking.k8s.io"
+ resources:
+ - "networkpolicies"
+ - "ingresses"
+- verbs:
+ - "*"
+ apiGroups:
+ - "policy"
+ resources:
+ - "poddisruptionbudgets"
+- verbs:
+ - "*"
+ apiGroups:
+ - ""
+ resources:
+ - "persistentvolumeclaims"
+ - "persistentvolumes"
+ - "configmaps"
+ - "pods"
+- verbs:
+ - "*"
+ apiGroups:
+ - "rbac.authorization.k8s.io"
+ resources:
+ - "roles"
+ - "rolebindings"
+ - "clusterroles"
+ - "clusterrolebindings"
+- verbs:
+ - "*"
+ apiGroups:
+ - "route.openshift.io"
+ resources:
+ - "routes"
+
+# +kubebuilder:scaffold:rules
diff --git a/operators/config/rbac/role_binding.yaml b/operators/config/rbac/role_binding.yaml
new file mode 100644
index 00000000..66361d05
--- /dev/null
+++ b/operators/config/rbac/role_binding.yaml
@@ -0,0 +1,15 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: manager-rolebinding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: manager-role
+subjects:
+- kind: ServiceAccount
+ name: controller-manager
+ namespace: system
diff --git a/operators/config/rbac/service_account.yaml b/operators/config/rbac/service_account.yaml
new file mode 100644
index 00000000..b24408f8
--- /dev/null
+++ b/operators/config/rbac/service_account.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ app.kubernetes.io/name: operators
+ app.kubernetes.io/managed-by: kustomize
+ name: controller-manager
+ namespace: system
diff --git a/operators/examples/geostudio-operator-template.yaml b/operators/examples/geostudio-operator-template.yaml
new file mode 100644
index 00000000..33ce35f0
--- /dev/null
+++ b/operators/examples/geostudio-operator-template.yaml
@@ -0,0 +1,491 @@
+# ==============================================================================
+# GEOStudio Operator CR Template
+# ==============================================================================
+# This template uses environment variable substitution for configuration.
+#
+# Usage:
+# 1. Ensure workspace//env/.env is configured with secrets
+# 2. Run: operators/deploy-geostudio-operator-lima.sh
+#
+# Or manually:
+# source workspace/lima/env/.env
+# source workspace/lima/env/env.sh
+# envsubst < operators/examples/geostudio-operator-template.yaml | kubectl apply -f -
+#
+# Variables are loaded from:
+# - workspace//env/.env (secrets and configuration)
+# - workspace//env/env.sh (environment-specific settings)
+#
+# See operators/examples/README.md for detailed documentation
+# ==============================================================================
+
+apiVersion: geostudio.geostudio.ibm.com/v1alpha1
+kind: GEOStudio
+metadata:
+ name: studio
+ namespace: ${ocp_project}
+spec:
+ # ==========================================================================
+ # UI Component
+ # ==========================================================================
+ geofm-ui:
+ enabled: true
+ image:
+ name: quay.io/geospatial-studio/geostudio-ui
+ tag: latest
+ route:
+ enabled: ${ROUTE_ENABLED}
+
+ # ==========================================================================
+ # Pipelines - ML Processing Workflows
+ # ==========================================================================
+ geospatial-studio-pipelines:
+ cluster_url: ${CLUSTER_URL}
+ default_replicas: 1
+ enabled: ${PIPELINES_ENABLED}
+ environment: ${ENVIRONMENT}
+
+ gateway:
+ api_key: ${studio_api_key}
+ appName: geofm-gateway
+ oauthProxyEnabled: ${OAUTH_PROXY_ENABLED}
+ oauthProxyPort: "${OAUTH_PROXY_PORT}"
+
+ geoserver:
+ appName: geofm-geoserver
+ password: ${geoserver_password}
+ username: ${geoserver_username}
+
+ gpuConfig:
+ enabled: ${CONFIGURE_GPU_AFFINITY_FLAG}
+
+ image:
+ pullPolicy: ${image_pull_policy}
+
+ imagePullSecret:
+ b64secret: ${image_pull_secret_b64}
+ create: true
+ name: us-icr-pull-secret
+
+ inference_volume:
+ pvc_name: inference-shared-pvc
+
+ log_level: DEBUG
+ namePrefix: pipelines
+ namespace: ${ocp_project}
+ nasaEarthBearerToken: ${nasa_earth_data_bearer_token}
+
+ objectStorage:
+ access_key: ${access_key_id}
+ buckets:
+ amo: ${BUCKET_AMO_INPUT_BUCKET}
+ datasetFactory: ${BUCKET_DATASET_FACTORY}
+ fineTuning: ${BUCKET_FINE_TUNING}
+ fineTuningModels: ${BUCKET_FINE_TUNING_MODELS}
+ geoserver: ${BUCKET_GEOSERVER}
+ inference: ${BUCKET_INFERENCE}
+ inference_auxdata: ${BUCKET_INFERENCE_AUXDATA}
+ mlflow: ${BUCKET_GFM_MLFLOW}
+ temporaryUploads: ${BUCKET_TEMP_UPLOAD}
+ generic_python_processor: ${BUCKET_GENERIC_PYTHON_PROCESSOR}
+ cos_storage_class: ${COS_STORAGE_CLASS}
+ create_bucket: false
+ createCosSecret: false
+ delete_bucket: false
+ endpoint: ${endpoint}
+ region: ${region}
+ secret_key: ${secret_access_key}
+
+ observability:
+ enabled: ${OBSERVABILITY_ENABLED}
+ otlp: {}
+
+ orchestrate_db:
+ inference_task_table: inf_task
+ pg_password: ${pg_password}
+ pg_port: ${pg_port}
+ pg_studio_db_name: ${pg_studio_db_name}
+ pg_uri: ${pg_uri}
+ pg_username: ${pg_username}
+ secret_name: pipelines-orchestration-db
+
+ # Pipeline Processors
+ # Note: SentinelHub and NASA credentials are inline values from .env
+ # If not using these services, leave sh_client_id, sh_client_secret,
+ # and nasa_earth_data_bearer_token empty in your .env file
+ processors:
+ # Inference Planner - Plans inference tasks
+ - enabled: true
+ extra_envs:
+ - name: bbox_to_tile_threshold_area
+ value: "1000000000000000000000000"
+ - name: SH_CLIENT_ID
+ value: "${sh_client_id}"
+ - name: SH_CLIENT_SECRET
+ value: "${sh_client_secret}"
+ - name: NASA_EARTH_BEARER_TOKEN
+ value: "${nasa_earth_data_bearer_token}"
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: inference-planner
+ process_exec: python inference_planner.py
+ process_id: inference-planner
+
+ # Terrakit Data Fetch - Fetches geospatial data
+ - enabled: true
+ extra_envs:
+ - name: SH_CLIENT_ID
+ value: "${sh_client_id}"
+ - name: SH_CLIENT_SECRET
+ value: "${sh_client_secret}"
+ - name: NASA_EARTH_BEARER_TOKEN
+ value: "${nasa_earth_data_bearer_token}"
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: terrakit-data-fetch
+ process_exec: python terrakit_data_fetch.py
+ process_id: terrakit-data-fetch
+
+ # Push to GeoServer - Publishes results to GeoServer
+ - enabled: true
+ extra_envs:
+ - name: geoserver_username
+ value: "${geoserver_username}"
+ - name: geoserver_password
+ value: "${geoserver_password}"
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: push-to-geoserver
+ process_exec: python push_to_geoserver.py
+ process_id: push-to-geoserver
+
+ # URL Connector - Connects to external data sources
+ - enabled: true
+ extra_envs:
+ - name: CHECK_DISK_FREE_SPACE
+ value: "FALSE"
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: url-connector
+ process_exec: python url_connector_single.py
+ process_id: url-connector
+
+ # Run Inference - Executes ML inference
+ - enabled: true
+ extra_envs:
+ - name: push_model_input
+ value: "True"
+ - name: mounted_pvc
+ value: "True"
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: run-inference
+ process_exec: python run-inference.py
+ process_id: run-inference
+
+ # Post-process Generic - Post-processes inference results
+ - enabled: true
+ extra_envs:
+ - name: LULC_TILE_ROOT
+ value: /auxdata/lulc/lc2021/
+ - name: LULC_TILE_SHAPEFILE
+ value: /auxdata/lulc/tiles.shp
+ - name: LAND_POLYGON_PATH
+ value: /auxdata/general/land_polygons.shp
+ extra_volumes:
+ - bucket_name: ${BUCKET_INFERENCE_AUXDATA}
+ claimName: inference-auxdata-pvc
+ create: true
+ mountPath: /auxdata
+ name: test-inference-auxdata-pvc
+ image:
+ name: quay.io/geospatial-studio/geostudio-pipelines
+ tag: latest
+ name: postprocess-generic
+ process_exec: python postprocess-generic-single.py
+ process_id: postprocess-generic
+ replicas: 1
+
+ # Terratorch Inference - Advanced geospatial ML inference
+ - enabled: true
+ extra_envs:
+ - name: PYTORCH_CUDA_ALLOC_CONF
+ value: expandable_segments:True
+ extra_volumes:
+ - bucket_name: ${BUCKET_FINE_TUNING}
+ claimName: gfm-ft-files-pvc
+ create: ${PIPELINES_TERRATORCH_INFERENCE_CREATE_FT_PVC}
+ mountPath: /tunes
+ name: tunes-pvc
+ image:
+ name: quay.io/geospatial-studio/terratorch
+ tag: latest
+ name: terratorch-inference
+ process_exec: python terratorch_inference.py
+ process_id: terratorch-inference
+ resources:
+ limits: {}
+ requests: {}
+ shm: "true"
+
+ route:
+ enabled: ${ROUTE_ENABLED}
+
+ sentinelhub:
+ client_id: ${sh_client_id}
+ client_secret: ${sh_client_secret}
+
+ serviceAccount:
+ name: geostudio-sa
+
+ storage:
+ filesystem:
+ dir: ${PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE}
+ enabled: ${STORAGE_FILESYSTEM_ENABLED}
+
+ waitForGateway:
+ enabled: true
+ image: busybox:1.36
+ port: ${OAUTH_PROXY_PORT}
+ sleepTime: 5
+
+ # ==========================================================================
+ # GeoServer - Geospatial Data Server (Optional)
+ # ==========================================================================
+ gfm-geoserver:
+ enabled: true
+ image:
+ repository: docker.osgeo.org/geoserver
+ tag: 2.28.1
+ persistence:
+ enabled: true
+ size: 20Gi
+ route:
+ enabled: ${ROUTE_ENABLED}
+ service:
+ port: 3000
+ targetPort: 8080
+
+ # ==========================================================================
+ # MLflow - Model Tracking
+ # ==========================================================================
+ gfm-mlflow:
+ enabled: true
+ route:
+ enabled: ${ROUTE_ENABLED}
+
+ # ==========================================================================
+ # Gateway - API Server
+ # ==========================================================================
+ gfm-studio-gateway:
+ enabled: true
+ image:
+ name: quay.io/geospatial-studio/geostudio-gateway
+ tag: latest
+ route:
+ enabled: ${ROUTE_ENABLED}
+ extraEnvironment:
+ api:
+ OBJECT_STORAGE_SIGNATURE_VERSION: s3v4
+ CREATE_TUNING_FOLDERS: ${CREATE_TUNING_FOLDERS_FLAG}
+ PIPELINES_V2_INFERENCE_ROOT_FOLDER: ${PIPELINES_V2_INFERENCE_ROOT_FOLDER_VALUE}
+ ENVIRONMENT: ${ENVIRONMENT}
+ CONFIGURE_GPU_AFFINITY: false
+
+ # ==========================================================================
+ # Global Configuration
+ # ==========================================================================
+ global:
+ redis:
+ password: ${redis_password}
+ fullnameOverride: ${REDIS_FULL_NAME_OVERRIDE}
+
+ backends:
+ cluster_url: ${CLUSTER_URL}
+ namespace: ${ocp_project}
+
+ cluster_url: ${CLUSTER_URL}
+ environment: ${ENVIRONMENT}
+
+ frontend:
+ cluster_url: ${CLUSTER_URL}
+ namespace: ${ocp_project}
+
+ imagePullPolicy: ${image_pull_policy}
+
+ imagePullSecret:
+ create: false
+
+ label: geofm-studio
+ namespace: ${ocp_project}
+ non_cos_storage_class: ${NON_COS_STORAGE_CLASS}
+
+ gfmStudioGateway:
+ api_key: ${studio_api_key}
+ api_encryption_key: ${studio_api_encryption_key}
+
+ jira:
+ api_key: ${jira_api_key}
+
+ oauth:
+ clientId: ${OAUTH_CLIENT_ID}
+ clientSecret: ${oauth_client_secret}
+ cookieSecret: ${oauth_cookie_secret}
+ createTLSSecret: ${CREATE_TLS_SECRET}
+ extraOauthProxyArgs:
+ - --upstream-timeout=900s
+ issuerUrl: ${OAUTH_ISSUER_URL}
+ oauthProxyEnabled: ${OAUTH_PROXY_ENABLED}
+ oauthProxyPort: "${OAUTH_PROXY_PORT}"
+ oauthUrl: ${OAUTH_URL}
+ tlsCrtB64: ${tls_crt_b64}
+ tlsKeyB64: ${tls_key_b64}
+ type: ${OAUTH_TYPE}
+
+ objectStorage:
+ access_key: ${access_key_id}
+ buckets:
+ amo: ${BUCKET_AMO_INPUT_BUCKET}
+ datasetFactory: ${BUCKET_DATASET_FACTORY}
+ fineTuning: ${BUCKET_FINE_TUNING}
+ fineTuningModels: ${BUCKET_FINE_TUNING_MODELS}
+ geoserver: ${BUCKET_GEOSERVER}
+ inference: ${BUCKET_INFERENCE}
+ inference_auxdata: ${BUCKET_INFERENCE_AUXDATA}
+ mlflow: ${BUCKET_GFM_MLFLOW}
+ temporaryUploads: ${BUCKET_TEMP_UPLOAD}
+ generic_python_processor: ${BUCKET_GENERIC_PYTHON_PROCESSOR}
+ cos_storage_class: ${COS_STORAGE_CLASS}
+ create_bucket: true
+ createCosSecret: false
+ endpoint: ${endpoint}
+ region: ${region}
+ secret_key: ${secret_access_key}
+
+ postgres:
+ backend_uri_base: postgresql+pg8000://${pg_username}:${pg_password}@${pg_uri}:${pg_port}
+ dbs:
+ auth: geostudio_auth
+ gateway: ${pg_studio_db_name}
+ geoserver: geoserver
+ mlflow: mlflow
+ postgres_db: postgres
+ postgres_host: ${pg_uri}
+ postgres_password: ${pg_password}
+ postgres_port: ${pg_port}
+ postgres_username: ${pg_username}
+
+ cesium:
+ token: ${cesium_token}
+
+ mapbox:
+ token: ${mapbox_token}
+
+ serviceAccount:
+ create: true
+ name: geostudio-sa
+
+ storage:
+ filesystem:
+ enabled: ${STORAGE_FILESYSTEM_ENABLED}
+ pvc:
+ enabled: ${STORAGE_PVC_ENABLED}
+
+ # ==========================================================================
+ # Infrastructure Resources - Auto-Deploy Services
+ # ==========================================================================
+
+ # PostgreSQL Configuration
+ postgresql:
+ enabled: true
+ chartVersion: "${PG_VERSION}"
+ fullnameOverride: "postgresql"
+ auth:
+ username: ${pg_username}
+ password: ${pg_password}
+ database: postgres
+ primary:
+ persistence:
+ enabled: true
+ size: 10Gi
+ storageClass: ""
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 250m
+ limits:
+ memory: 1Gi
+ cpu: 1000m
+
+ # MinIO Configuration
+ minio:
+ enabled: true
+ image: ${MINIO_IMAGE}
+ tag: ${MINIO_TAG}
+ auth:
+ rootUser: ${access_key_id}
+ rootPassword: ${secret_access_key}
+ persistence:
+ enabled: ${MINIO_PERSISTENCE_ENABLED}
+ size: ${MINIO_STORAGE_SIZE}
+ storageClass: ${NON_COS_STORAGE_CLASS}
+ resources: {}
+
+ # Keycloak Configuration
+ keycloak:
+ enabled: true
+ image: quay.io/keycloak/keycloak
+ tag: "26.4.5"
+ database: keycloak
+ realm: geostudio
+ clientSecret: oauth_client_secret
+ auth:
+ adminUser: ${keycloak_admin_user}
+ adminPassword: ${keycloak_admin_password}
+ testUser:
+ username: testuser
+ password: testpass123
+ email: test@example.com
+ firstName: Test
+ lastName: User
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 250m
+ limits:
+ memory: 1Gi
+ cpu: 500m
+
+ # GeoServer Configuration
+ geoserver:
+ enabled: true
+ image: docker.osgeo.org/geoserver:2.28.1
+ adminUsername: ${geoserver_username}
+ adminPassword: ${geoserver_password}
+ javaOpts: ${GEOSERVER_CM_PROXYBASEURL}
+ storage: 60Gi
+ storageClass: ${NON_COS_STORAGE_CLASS}
+ resources: {}
+
+ # CSI Driver Configuration
+ csiDriver:
+ enabled: true
+ type: "${CSI_DRIVER_TYPE}"
+
+ # ==========================================================================
+ # Redis - Cache and Message Broker
+ # ==========================================================================
+ redis:
+ architecture: ${REDIS_ARCHITECTURE}
+ auth:
+ enabled: ${REDIS_ENABLED}
+ password: ${redis_password}
+ enabled: ${REDIS_ENABLED}
+ fullnameOverride: ${REDIS_FULL_NAME_OVERRIDE}
+ password: ${redis_password}
+
diff --git a/operators/watches.yaml b/operators/watches.yaml
new file mode 100644
index 00000000..d90a4e17
--- /dev/null
+++ b/operators/watches.yaml
@@ -0,0 +1,9 @@
+# Use the 'create api' subcommand to add watches to this file.
+- group: geostudio.geostudio.ibm.com
+ version: v1alpha1
+ kind: GEOStudio
+ chart: helm-charts/geospatial-studio
+ watchDependentResources: false
+ overrideValues:
+ maxHistory: 3
+# +kubebuilder:scaffold:watch