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 + +![Demo GIF showing the deployment process](assets/operator-install.gif) + +### 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: "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" + 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: "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" + 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