diff --git a/justfile b/justfile index 0d6c9fb..6a02370 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,7 @@ mod keycloak mod jupyterhub mod k8s mod longhorn +mod minio mod postgres mod utils mod vault diff --git a/minio/.gitignore b/minio/.gitignore new file mode 100644 index 0000000..08d4bbf --- /dev/null +++ b/minio/.gitignore @@ -0,0 +1 @@ +minio-values.yaml diff --git a/minio/justfile b/minio/justfile new file mode 100644 index 0000000..9c02f29 --- /dev/null +++ b/minio/justfile @@ -0,0 +1,116 @@ +set fallback := true + +export MINIO_NAMESPACE := env("MINIO_NAMESPACE", "minio") +export MINIO_CHART_VERSION := env("MINIO_CHART_VERSION", "5.4.0") +export MINIO_OIDC_CLIENT_ID := env("MINIO_OIDC_CLIENT_ID", "minio") +export KEYCLOAK_REALM := env("KEYCLOAK_REALM", "buunstack") + +[private] +default: + @just --list --unsorted --list-submodules + +# Add Helm repository +add-helm-repo: + # We use charts.min.io instead of operator.min.io because the operator does not support + # standalone mode. + # helm repo add minio https://operator.min.io/ + helm repo add minio https://charts.min.io/ + helm repo update + +# Remove Helm repository +remove-helm-repo: + helm repo remove minio + +# Create JupyterHub namespace +create-namespace: + kubectl get namespace ${MINIO_NAMESPACE} &>/dev/null || \ + kubectl create namespace ${MINIO_NAMESPACE} + +# Delete JupyterHub namespace +delete-namespace: + kubectl delete namespace ${MINIO_NAMESPACE} --ignore-not-found + +# Add Keycloak policy and mapper +add-keycloak-minio-policy: + KEYCLOAK_ADMIN_USER=$(just keycloak::admin-username) \ + KEYCLOAK_ADMIN_PASSWORD=$(just keycloak::admin-password) \ + KEYCLOAK_REALM=${KEYCLOAK_REALM} \ + MINIO_OIDC_CLIENT_ID=${MINIO_OIDC_CLIENT_ID} \ + dotenvx run -f ../.env.local -- tsx ./scripts/add-minio-policy.ts + +# Install MinIO +install: + #!/bin/bash + set -euo pipefail + export MINIO_HOST=${MINIO_HOST:-} + if [ "${MINIO_HOST}" = "" ]; then + MINIO_HOST=$( + gum input --prompt="MinIO host (FQDN): " --width=100 \ + --placeholder="e.g., minio.example.com" + ) + fi + export MINIO_CONSOLE_HOST=${MINIO_CONSOLE_HOST:-} + if [ "${MINIO_CONSOLE_HOST}" = "" ]; then + MINIO_CONSOLE_HOST=$( + gum input --prompt="MinIO Console host (FQDN): " --width=100 \ + --placeholder="e.g., minio-console.example.com" + ) + fi + just keycloak::create-client ${KEYCLOAK_REALM} ${MINIO_OIDC_CLIENT_ID} \ + "https://${MINIO_HOST}/oauth_callback,https://${MINIO_CONSOLE_HOST}/oauth_callback" + just add-keycloak-minio-policy + just create-namespace + just add-helm-repo + gomplate -f minio-values.gomplate.yaml -o minio-values.yaml + helm upgrade --install minio minio/minio \ + --version ${MINIO_CHART_VERSION} -n ${MINIO_NAMESPACE} --create-namespace --wait \ + -f minio-values.yaml + +# Uninstall MinIO +uninstall: + helm uninstall minio -n ${MINIO_NAMESPACE} --wait --ignore-not-found + kubectl delete namespace ${MINIO_NAMESPACE} --ignore-not-found + +# List MinIO internal policies and users (for debugging) +debug-info: + @kubectl -n ${MINIO_NAMESPACE} exec -it deploy/minio -- \ + bash -c "mc alias set local http://localhost:9000 $(just root-user) $(just root-password) && \ + echo '--- Policies ---' && \ + mc admin policy list local && \ + echo '--- Users ---' && \ + mc admin user list local" + +# Print MinIO root user +root-user: + @kubectl -n ${MINIO_NAMESPACE} get secret minio -o jsonpath='{.data.rootUser}' | base64 -d + @echo + +# Print MinIO root password +root-password: + @kubectl -n ${MINIO_NAMESPACE} get secret minio -o jsonpath='{.data.rootPassword}' | base64 -d + @echo + +# Create a bucket +create-bucket bucket: + #!/bin/bash + set -euo pipefail + ROOT_USER=$(just root-user) + ROOT_PASSWORD=$(just root-password) + kubectl -n ${MINIO_NAMESPACE} exec -it deploy/minio -- \ + bash -c "mc alias set local http://localhost:9000 ${ROOT_USER} ${ROOT_PASSWORD} && \ + mc mb --ignore-existing local/{{ bucket }}" + +# Check if a bucket exists (returns exit code 0 if exists, 1 if not) +[no-exit-message] +bucket-exists bucket: + #!/bin/bash + set -euo pipefail + ROOT_USER=$(just root-user) + ROOT_PASSWORD=$(just root-password) + if kubectl -n ${MINIO_NAMESPACE} exec -it deploy/minio -- \ + bash -c "mc alias set local http://localhost:9000 ${ROOT_USER} ${ROOT_PASSWORD} >/dev/null 2>&1 && \ + mc ls local/{{ bucket }} >/dev/null 2>&1"; then + exit 0 # Bucket exists + else + exit 1 # Bucket does not exist + fi diff --git a/minio/minio-values.gomplate.yaml b/minio/minio-values.gomplate.yaml new file mode 100644 index 0000000..7c33697 --- /dev/null +++ b/minio/minio-values.gomplate.yaml @@ -0,0 +1,40 @@ +mode: standalone + +clusterDomain: {{ .Env.MINIO_HOST }} + +oidc: + enabled: true + configUrl: "https://{{ .Env.KEYCLOAK_HOST }}/realms/{{ .Env.KEYCLOAK_REALM }}/.well-known/openid-configuration" + clientId: "{{ .Env.MINIO_OIDC_CLIENT_ID }}" + clientSecret: "" + claimName: "minioPolicy" + scopes: "openid,profile,email" + redirectUri: "https://{{ .Env.MINIO_CONSOLE_HOST }}/oauth_callback" + displayName: "Login with Keycloak" + +persistence: + size: 50Gi + +ingress: + enabled: true + ingressClassName: traefik + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: websecure + hosts: + - {{ .Env.MINIO_HOST }} + tls: + - hosts: + - {{ .Env.MINIO_HOST }} + +consoleIngress: + enabled: true + ingressClassName: traefik + annotations: + kubernetes.io/ingress.class: traefik + traefik.ingress.kubernetes.io/router.entrypoints: websecure + hosts: + - {{ .Env.MINIO_CONSOLE_HOST }} + tls: + - hosts: + - {{ .Env.MINIO_CONSOLE_HOST }} diff --git a/minio/scripts/add-minio-policy.ts b/minio/scripts/add-minio-policy.ts new file mode 100644 index 0000000..4e2466a --- /dev/null +++ b/minio/scripts/add-minio-policy.ts @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +// This script is a wrapper for add-attribute-mapper.ts specifically for MinIO policy configuration +// It sets the appropriate environment variables and calls the generic script + +import { spawn } from "node:child_process"; +import invariant from "tiny-invariant"; + +const main = async () => { + // Validate MinIO-specific environment variables + const minioClientId = process.env.MINIO_OIDC_CLIENT_ID; + invariant(minioClientId, "MINIO_OIDC_CLIENT_ID environment variable is required"); + + const policyValue = process.env.MINIO_POLICY || "readwrite"; + console.log(`Setting MinIO policy attribute with default value: ${policyValue}`); + + // Set up environment variables for the generic script + const env = { + ...process.env, + CLIENT_ID: minioClientId, + ATTRIBUTE_NAME: "minioPolicy", + ATTRIBUTE_DISPLAY_NAME: "MinIO Policy", + ATTRIBUTE_CLAIM_NAME: "minioPolicy", + ATTRIBUTE_OPTIONS: "readwrite,readonly,writeonly", + ATTRIBUTE_DEFAULT_VALUE: policyValue, + MAPPER_NAME: "MinIO Policy", + }; + + // Call the generic add-attribute-mapper script + const child = spawn("npx", ["tsx", "../../keycloak/scripts/add-attribute-mapper.ts"], { + cwd: __dirname, + env, + stdio: "inherit", + }); + + child.on("error", (error) => { + console.error("Failed to execute add-attribute-mapper.ts:", error); + process.exit(1); + }); + + child.on("exit", (code) => { + process.exit(code || 0); + }); +}; + +main();