From 54e067ed54fac854e03f39bdc90e70b4af4eff4a Mon Sep 17 00:00:00 2001 From: apescale Date: Mon, 31 Aug 2026 06:43:31 +0800 Subject: [PATCH] fix(images): normalize shell scripts to LF line endings database image crashed at start: CRLF shebang in start.sh made the kernel look for interpreter '/bin/bash\r'. Other image Dockerfiles mask this with dos2unix at build time, but scripts/images/database/Dockerfile does not. Convert all tracked .sh files to LF and pin eol=lf via .gitattributes. --- .gitattributes | 2 + .../deploy/docker-entrypoint.sh | 46 +- runtime/deepdata-python/uvicorn_start.sh | 12 +- scripts/db/zz-final-task.sh | 42 +- scripts/images/backend/start.sh | 16 +- .../images/common/log-rotate-copytruncate.sh | 194 ++-- .../images/database/prune-postgres-logs.sh | 64 +- scripts/images/database/start.sh | 14 +- scripts/images/frontend/rotate-nginx-logs.sh | 184 +-- scripts/images/frontend/start.sh | 124 +- scripts/images/gateway/start.sh | 10 +- scripts/images/runtime-ray/start.sh | 70 +- scripts/images/runtime/start.sh | 24 +- scripts/k8s/collect-secrets.sh | 496 ++++---- scripts/k8s/node-cleanup.sh | 334 +++--- scripts/k8s/node-setup.sh | 1008 ++++++++--------- 16 files changed, 1321 insertions(+), 1319 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f009d26 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +*.bat text eol=crlf diff --git a/runtime/deepdata-python/deploy/docker-entrypoint.sh b/runtime/deepdata-python/deploy/docker-entrypoint.sh index d3659db..f08dc3d 100644 --- a/runtime/deepdata-python/deploy/docker-entrypoint.sh +++ b/runtime/deepdata-python/deploy/docker-entrypoint.sh @@ -1,23 +1,23 @@ -#!/bin/bash -set -e - -if [ -d "${LOCAL_FILES_DOCUMENT_ROOT}" ] && [ "${LOCAL_FILES_SERVING_ENABLED}" = "true" ]; then - echo "Using local document root: ${LOCAL_FILES_DOCUMENT_ROOT}" -fi - -# 启动应用 -echo "==========================================" -echo "Starting DeepData Backend Service..." -echo "Host: ${HOST:-0.0.0.0}" -echo "Port: ${PORT:-18000}" -echo "Debug: ${DEBUG:-false}" - -# 转换 LOG_LEVEL 为小写(uvicorn 要求小写) -LOG_LEVEL_LOWER=$(echo "${LOG_LEVEL:-info}" | tr '[:upper:]' '[:lower:]') - -# 使用 uvicorn 启动应用 -exec uvicorn app.main:app \ - --host "${HOST:-0.0.0.0}" \ - --port "${PORT:-18000}" \ - --log-level "${LOG_LEVEL_LOWER}" \ - ${DEBUG:+--reload} +#!/bin/bash +set -e + +if [ -d "${LOCAL_FILES_DOCUMENT_ROOT}" ] && [ "${LOCAL_FILES_SERVING_ENABLED}" = "true" ]; then + echo "Using local document root: ${LOCAL_FILES_DOCUMENT_ROOT}" +fi + +# 启动应用 +echo "==========================================" +echo "Starting DeepData Backend Service..." +echo "Host: ${HOST:-0.0.0.0}" +echo "Port: ${PORT:-18000}" +echo "Debug: ${DEBUG:-false}" + +# 转换 LOG_LEVEL 为小写(uvicorn 要求小写) +LOG_LEVEL_LOWER=$(echo "${LOG_LEVEL:-info}" | tr '[:upper:]' '[:lower:]') + +# 使用 uvicorn 启动应用 +exec uvicorn app.main:app \ + --host "${HOST:-0.0.0.0}" \ + --port "${PORT:-18000}" \ + --log-level "${LOG_LEVEL_LOWER}" \ + ${DEBUG:+--reload} diff --git a/runtime/deepdata-python/uvicorn_start.sh b/runtime/deepdata-python/uvicorn_start.sh index 03506c3..8a60af5 100644 --- a/runtime/deepdata-python/uvicorn_start.sh +++ b/runtime/deepdata-python/uvicorn_start.sh @@ -1,7 +1,7 @@ -export LOG_LEVEL=DEBUG -export DEBUG=true - -uvicorn app.main:app \ - --host 0.0.0.0 \ - --port 18000 \ +export LOG_LEVEL=DEBUG +export DEBUG=true + +uvicorn app.main:app \ + --host 0.0.0.0 \ + --port 18000 \ --reload \ No newline at end of file diff --git a/scripts/db/zz-final-task.sh b/scripts/db/zz-final-task.sh index d00e73a..bb1ef49 100644 --- a/scripts/db/zz-final-task.sh +++ b/scripts/db/zz-final-task.sh @@ -1,22 +1,22 @@ -#!/bin/bash -# 遇到错误即退出 -set -e - -# 使用 -n 判断环境变量 HOME_PAGE_URL 是否非空(即已设置且有值) -if [ -n "$HOME_PAGE_URL" ]; then - echo "检测到 HOME_PAGE_URL 环境变量已配置,值为: $HOME_PAGE_URL。准备更新数据库..." - - # 只有变量非空时,才会进入这里执行 SQL - psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "deepdata" -v my_url="$HOME_PAGE_URL" <<-EOSQL - - UPDATE t_sys_param - SET param_value = :'my_url' - WHERE id = 'sys.home.page.url'; - -EOSQL - - echo "sys.home.page.url 更新完成!" -else - # 如果变量为空或未设置,打印提示并直接跳过 - echo "未配置 HOME_PAGE_URL 环境变量或值为空,跳过 sys.home.page.url 的更新操作。" +#!/bin/bash +# 遇到错误即退出 +set -e + +# 使用 -n 判断环境变量 HOME_PAGE_URL 是否非空(即已设置且有值) +if [ -n "$HOME_PAGE_URL" ]; then + echo "检测到 HOME_PAGE_URL 环境变量已配置,值为: $HOME_PAGE_URL。准备更新数据库..." + + # 只有变量非空时,才会进入这里执行 SQL + psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "deepdata" -v my_url="$HOME_PAGE_URL" <<-EOSQL + + UPDATE t_sys_param + SET param_value = :'my_url' + WHERE id = 'sys.home.page.url'; + +EOSQL + + echo "sys.home.page.url 更新完成!" +else + # 如果变量为空或未设置,打印提示并直接跳过 + echo "未配置 HOME_PAGE_URL 环境变量或值为空,跳过 sys.home.page.url 的更新操作。" fi \ No newline at end of file diff --git a/scripts/images/backend/start.sh b/scripts/images/backend/start.sh index d9e0332..b3c7cfb 100644 --- a/scripts/images/backend/start.sh +++ b/scripts/images/backend/start.sh @@ -1,8 +1,8 @@ -#!/bin/bash - -set -e - -umask 0022 - -echo "Starting main application..." -exec "$@" +#!/bin/bash + +set -e + +umask 0022 + +echo "Starting main application..." +exec "$@" diff --git a/scripts/images/common/log-rotate-copytruncate.sh b/scripts/images/common/log-rotate-copytruncate.sh index 238dd1e..45e2c1d 100644 --- a/scripts/images/common/log-rotate-copytruncate.sh +++ b/scripts/images/common/log-rotate-copytruncate.sh @@ -1,97 +1,97 @@ -#!/bin/bash - -set -euo pipefail - -run_once=false -if [ "${1:-}" = "--once" ]; then - run_once=true - shift -fi - -LOG_ROOT="${1:-${LOG_ROTATION_PATH:-/tmp/ray}}" -LOG_ROTATION_MAX_SIZE="${LOG_ROTATION_MAX_SIZE:-100MB}" -LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" -LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" - -parse_size_bytes() { - local value - value="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" - value="${value// /}" - case "$value" in - *GB) echo $((${value%GB} * 1024 * 1024 * 1024)) ;; - *G) echo $((${value%G} * 1024 * 1024 * 1024)) ;; - *MB) echo $((${value%MB} * 1024 * 1024)) ;; - *M) echo $((${value%M} * 1024 * 1024)) ;; - *KB) echo $((${value%KB} * 1024)) ;; - *K) echo $((${value%K} * 1024)) ;; - *B) echo "${value%B}" ;; - *) echo "$value" ;; - esac -} - -file_size_bytes() { - wc -c < "$1" | tr -d ' ' -} - -backup_path() { - local file="$1" - local index="$2" - local ext="${file##*.}" - local stem="${file%.$ext}" - echo "${stem}.${index}.${ext}" -} - -is_backup_file() { - local name - name="$(basename "$1")" - [[ "$name" =~ \.[0-9]+\.(log|out|err)$ ]] -} - -rotate_file() { - local file="$1" - local max_bytes="$2" - local backup_count="$3" - - [ -f "$file" ] || return 0 - is_backup_file "$file" && return 0 - - local current_size - current_size="$(file_size_bytes "$file")" - [ "$current_size" -lt "$max_bytes" ] && return 0 - - rm -f "$(backup_path "$file" "$backup_count")" - - local index - for ((index = backup_count - 1; index >= 1; index--)); do - local src dst - src="$(backup_path "$file" "$index")" - dst="$(backup_path "$file" "$((index + 1))")" - [ -f "$src" ] && mv "$src" "$dst" - done - - cp -p "$file" "$(backup_path "$file" 1)" - : > "$file" -} - -rotate_once() { - [ -d "$LOG_ROOT" ] || return 0 - - local max_bytes - max_bytes="$(parse_size_bytes "$LOG_ROTATION_MAX_SIZE")" - - while IFS= read -r -d '' file; do - rotate_file "$file" "$max_bytes" "$LOG_ROTATION_BACKUP_COUNT" - done < <( - find "$LOG_ROOT" -type f \( -name '*.log' -o -name '*.out' -o -name '*.err' \) -print0 2>/dev/null || true - ) -} - -if [ "$run_once" = true ]; then - rotate_once - exit 0 -fi - -while true; do - rotate_once - sleep "$LOG_ROTATION_INTERVAL_SECONDS" -done +#!/bin/bash + +set -euo pipefail + +run_once=false +if [ "${1:-}" = "--once" ]; then + run_once=true + shift +fi + +LOG_ROOT="${1:-${LOG_ROTATION_PATH:-/tmp/ray}}" +LOG_ROTATION_MAX_SIZE="${LOG_ROTATION_MAX_SIZE:-100MB}" +LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" +LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" + +parse_size_bytes() { + local value + value="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" + value="${value// /}" + case "$value" in + *GB) echo $((${value%GB} * 1024 * 1024 * 1024)) ;; + *G) echo $((${value%G} * 1024 * 1024 * 1024)) ;; + *MB) echo $((${value%MB} * 1024 * 1024)) ;; + *M) echo $((${value%M} * 1024 * 1024)) ;; + *KB) echo $((${value%KB} * 1024)) ;; + *K) echo $((${value%K} * 1024)) ;; + *B) echo "${value%B}" ;; + *) echo "$value" ;; + esac +} + +file_size_bytes() { + wc -c < "$1" | tr -d ' ' +} + +backup_path() { + local file="$1" + local index="$2" + local ext="${file##*.}" + local stem="${file%.$ext}" + echo "${stem}.${index}.${ext}" +} + +is_backup_file() { + local name + name="$(basename "$1")" + [[ "$name" =~ \.[0-9]+\.(log|out|err)$ ]] +} + +rotate_file() { + local file="$1" + local max_bytes="$2" + local backup_count="$3" + + [ -f "$file" ] || return 0 + is_backup_file "$file" && return 0 + + local current_size + current_size="$(file_size_bytes "$file")" + [ "$current_size" -lt "$max_bytes" ] && return 0 + + rm -f "$(backup_path "$file" "$backup_count")" + + local index + for ((index = backup_count - 1; index >= 1; index--)); do + local src dst + src="$(backup_path "$file" "$index")" + dst="$(backup_path "$file" "$((index + 1))")" + [ -f "$src" ] && mv "$src" "$dst" + done + + cp -p "$file" "$(backup_path "$file" 1)" + : > "$file" +} + +rotate_once() { + [ -d "$LOG_ROOT" ] || return 0 + + local max_bytes + max_bytes="$(parse_size_bytes "$LOG_ROTATION_MAX_SIZE")" + + while IFS= read -r -d '' file; do + rotate_file "$file" "$max_bytes" "$LOG_ROTATION_BACKUP_COUNT" + done < <( + find "$LOG_ROOT" -type f \( -name '*.log' -o -name '*.out' -o -name '*.err' \) -print0 2>/dev/null || true + ) +} + +if [ "$run_once" = true ]; then + rotate_once + exit 0 +fi + +while true; do + rotate_once + sleep "$LOG_ROTATION_INTERVAL_SECONDS" +done diff --git a/scripts/images/database/prune-postgres-logs.sh b/scripts/images/database/prune-postgres-logs.sh index 664d05f..834d463 100644 --- a/scripts/images/database/prune-postgres-logs.sh +++ b/scripts/images/database/prune-postgres-logs.sh @@ -1,32 +1,32 @@ -#!/bin/bash - -set -euo pipefail - -LOG_DIR="${POSTGRES_LOG_DIR:-/var/log/deepdata/database}" -LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" -LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" - -prune_once() { - [ -d "$LOG_DIR" ] || return 0 - - mapfile -t files < <( - find "$LOG_DIR" -maxdepth 1 -type f -name 'postgresql-*.log' -printf '%T@ %p\n' 2>/dev/null \ - | sort -rn \ - | sed 's/^[^ ]* //' - ) - - local index - for ((index = LOG_ROTATION_BACKUP_COUNT; index < ${#files[@]}; index++)); do - rm -f "${files[$index]}" - done -} - -if [ "${1:-}" = "--once" ]; then - prune_once - exit 0 -fi - -while true; do - prune_once - sleep "$LOG_ROTATION_INTERVAL_SECONDS" -done +#!/bin/bash + +set -euo pipefail + +LOG_DIR="${POSTGRES_LOG_DIR:-/var/log/deepdata/database}" +LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" +LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" + +prune_once() { + [ -d "$LOG_DIR" ] || return 0 + + mapfile -t files < <( + find "$LOG_DIR" -maxdepth 1 -type f -name 'postgresql-*.log' -printf '%T@ %p\n' 2>/dev/null \ + | sort -rn \ + | sed 's/^[^ ]* //' + ) + + local index + for ((index = LOG_ROTATION_BACKUP_COUNT; index < ${#files[@]}; index++)); do + rm -f "${files[$index]}" + done +} + +if [ "${1:-}" = "--once" ]; then + prune_once + exit 0 +fi + +while true; do + prune_once + sleep "$LOG_ROTATION_INTERVAL_SECONDS" +done diff --git a/scripts/images/database/start.sh b/scripts/images/database/start.sh index 2f89853..41b9402 100644 --- a/scripts/images/database/start.sh +++ b/scripts/images/database/start.sh @@ -1,7 +1,7 @@ -#!/bin/bash - -set -e - -/usr/local/bin/prune-postgres-logs.sh & - -exec docker-entrypoint.sh "$@" +#!/bin/bash + +set -e + +/usr/local/bin/prune-postgres-logs.sh & + +exec docker-entrypoint.sh "$@" diff --git a/scripts/images/frontend/rotate-nginx-logs.sh b/scripts/images/frontend/rotate-nginx-logs.sh index da53764..723af0e 100644 --- a/scripts/images/frontend/rotate-nginx-logs.sh +++ b/scripts/images/frontend/rotate-nginx-logs.sh @@ -1,92 +1,92 @@ -#!/bin/bash - -set -euo pipefail - -LOG_DIR="${NGINX_LOG_DIR:-/var/log/deepdata/frontend}" -LOG_ROTATION_MAX_SIZE="${LOG_ROTATION_MAX_SIZE:-100MB}" -LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" -LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" - -parse_size_bytes() { - local value - value="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" - value="${value// /}" - case "$value" in - *GB) echo $((${value%GB} * 1024 * 1024 * 1024)) ;; - *G) echo $((${value%G} * 1024 * 1024 * 1024)) ;; - *MB) echo $((${value%MB} * 1024 * 1024)) ;; - *M) echo $((${value%M} * 1024 * 1024)) ;; - *KB) echo $((${value%KB} * 1024)) ;; - *K) echo $((${value%K} * 1024)) ;; - *B) echo "${value%B}" ;; - *) echo "$value" ;; - esac -} - -file_size_bytes() { - wc -c < "$1" | tr -d ' ' -} - -backup_path() { - local file="$1" - local index="$2" - local ext="${file##*.}" - local stem="${file%.$ext}" - echo "${stem}.${index}.${ext}" -} - -rotate_file() { - local file="$1" - local max_bytes="$2" - local backup_count="$3" - - [ -f "$file" ] || return 1 - - local current_size - current_size="$(file_size_bytes "$file")" - [ "$current_size" -lt "$max_bytes" ] && return 1 - - rm -f "$(backup_path "$file" "$backup_count")" - - local index - for ((index = backup_count - 1; index >= 1; index--)); do - local src dst - src="$(backup_path "$file" "$index")" - dst="$(backup_path "$file" "$((index + 1))")" - [ -f "$src" ] && mv "$src" "$dst" - done - - mv "$file" "$(backup_path "$file" 1)" - install -o nginx -g nginx -m 0644 /dev/null "$file" - return 0 -} - -rotate_once() { - mkdir -p "$LOG_DIR" - touch "$LOG_DIR/access.log" "$LOG_DIR/error.log" - chown -R nginx:nginx "$LOG_DIR" 2>/dev/null || true - - local max_bytes - max_bytes="$(parse_size_bytes "$LOG_ROTATION_MAX_SIZE")" - - local rotated=false - for file in "$LOG_DIR/access.log" "$LOG_DIR/error.log"; do - if rotate_file "$file" "$max_bytes" "$LOG_ROTATION_BACKUP_COUNT"; then - rotated=true - fi - done - - if [ "$rotated" = true ]; then - nginx -s reopen || true - fi -} - -if [ "${1:-}" = "--once" ]; then - rotate_once - exit 0 -fi - -while true; do - rotate_once - sleep "$LOG_ROTATION_INTERVAL_SECONDS" -done +#!/bin/bash + +set -euo pipefail + +LOG_DIR="${NGINX_LOG_DIR:-/var/log/deepdata/frontend}" +LOG_ROTATION_MAX_SIZE="${LOG_ROTATION_MAX_SIZE:-100MB}" +LOG_ROTATION_BACKUP_COUNT="${LOG_ROTATION_BACKUP_COUNT:-30}" +LOG_ROTATION_INTERVAL_SECONDS="${LOG_ROTATION_INTERVAL_SECONDS:-300}" + +parse_size_bytes() { + local value + value="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')" + value="${value// /}" + case "$value" in + *GB) echo $((${value%GB} * 1024 * 1024 * 1024)) ;; + *G) echo $((${value%G} * 1024 * 1024 * 1024)) ;; + *MB) echo $((${value%MB} * 1024 * 1024)) ;; + *M) echo $((${value%M} * 1024 * 1024)) ;; + *KB) echo $((${value%KB} * 1024)) ;; + *K) echo $((${value%K} * 1024)) ;; + *B) echo "${value%B}" ;; + *) echo "$value" ;; + esac +} + +file_size_bytes() { + wc -c < "$1" | tr -d ' ' +} + +backup_path() { + local file="$1" + local index="$2" + local ext="${file##*.}" + local stem="${file%.$ext}" + echo "${stem}.${index}.${ext}" +} + +rotate_file() { + local file="$1" + local max_bytes="$2" + local backup_count="$3" + + [ -f "$file" ] || return 1 + + local current_size + current_size="$(file_size_bytes "$file")" + [ "$current_size" -lt "$max_bytes" ] && return 1 + + rm -f "$(backup_path "$file" "$backup_count")" + + local index + for ((index = backup_count - 1; index >= 1; index--)); do + local src dst + src="$(backup_path "$file" "$index")" + dst="$(backup_path "$file" "$((index + 1))")" + [ -f "$src" ] && mv "$src" "$dst" + done + + mv "$file" "$(backup_path "$file" 1)" + install -o nginx -g nginx -m 0644 /dev/null "$file" + return 0 +} + +rotate_once() { + mkdir -p "$LOG_DIR" + touch "$LOG_DIR/access.log" "$LOG_DIR/error.log" + chown -R nginx:nginx "$LOG_DIR" 2>/dev/null || true + + local max_bytes + max_bytes="$(parse_size_bytes "$LOG_ROTATION_MAX_SIZE")" + + local rotated=false + for file in "$LOG_DIR/access.log" "$LOG_DIR/error.log"; do + if rotate_file "$file" "$max_bytes" "$LOG_ROTATION_BACKUP_COUNT"; then + rotated=true + fi + done + + if [ "$rotated" = true ]; then + nginx -s reopen || true + fi +} + +if [ "${1:-}" = "--once" ]; then + rotate_once + exit 0 +fi + +while true; do + rotate_once + sleep "$LOG_ROTATION_INTERVAL_SECONDS" +done diff --git a/scripts/images/frontend/start.sh b/scripts/images/frontend/start.sh index b738529..e6463d2 100644 --- a/scripts/images/frontend/start.sh +++ b/scripts/images/frontend/start.sh @@ -1,62 +1,62 @@ -#!/bin/bash - -function set_proxy_protocol() { - if [ "${REAL_IP_MODE}" != "proxy_protocol" ]; then - echo "REAL_IP_MODE is ${REAL_IP_MODE}, no need to update nginx configuration file." - return 0 - fi - sed -i 's/listen 3000;/listen 3000 proxy_protocol;/' /opt/frontend/http_backend.conf - sed -i 's/listen 3000 ssl;/listen 3000 ssl proxy_protocol;/' /opt/frontend/https_backend.conf - sed -i '/access_log.*main/a\ set_real_ip_from 0.0.0.0/0;' /opt/frontend/nginx.conf - sed -i '/access_log.*main/a\ real_ip_header proxy_protocol;' /opt/frontend/nginx.conf - echo "Nginx configuration file updated." -} - -mkdir -p /var/log/deepdata/frontend -touch /var/log/deepdata/frontend/access.log /var/log/deepdata/frontend/error.log -chown -R nginx:nginx /var/log/deepdata/frontend 2>/dev/null || true -/usr/local/bin/rotate-nginx-logs.sh & - -if [ -f "/cert/server.pem" ]; then - cp /cert/server.pem /etc/nginx/cert/server.pem - chown nginx:nginx /etc/nginx/cert/server.pem -fi - -if [ -f "/cert/server.key" ]; then - # Private key MUST be encrypted. Plaintext keys are not accepted. - if grep -q "ENCRYPTED" /cert/server.key 2>/dev/null; then - if [ -z "$CERT_PASS" ]; then - echo "ERROR: CERT_PASS is required to decrypt the private key" - exit 1 - fi - echo "$CERT_PASS" | openssl pkey -in /cert/server.key -out /etc/nginx/cert/server.key -passin stdin - chmod 600 /etc/nginx/cert/server.key - else - echo "ERROR: /cert/server.key is not encrypted. Please encrypt it before deployment." - exit 1 - fi - chown nginx:nginx /etc/nginx/cert/server.key -fi - -set_proxy_protocol -cp /opt/frontend/nginx.conf /etc/nginx/nginx.conf -if [ -f "/etc/nginx/cert/server.pem" ]; then - cp /opt/frontend/https_backend.conf /etc/nginx/conf.d/default.conf - cp /opt/frontend/routes.inc /etc/nginx/conf.d/routes.inc - echo "Switching to HTTPS config" -else - cp /opt/frontend/http_backend.conf /etc/nginx/conf.d/default.conf - cp /opt/frontend/routes.inc /etc/nginx/conf.d/routes.inc - - if [ -n "$DOMAIN" ]; then - cron - certbot --nginx "-d ${DOMAIN//,/ -d }" - echo "Switching to HTTPS config, Domain: $DOMAIN" - echo "0 0 1 * * root /usr/local/bin/certbot renew --quiet" | tee /etc/cron.d/certbot-renew - chmod 0644 /etc/cron.d/certbot-renew - else - echo "Switching to HTTP config" - fi -fi - -exec nginx -g "daemon off;" +#!/bin/bash + +function set_proxy_protocol() { + if [ "${REAL_IP_MODE}" != "proxy_protocol" ]; then + echo "REAL_IP_MODE is ${REAL_IP_MODE}, no need to update nginx configuration file." + return 0 + fi + sed -i 's/listen 3000;/listen 3000 proxy_protocol;/' /opt/frontend/http_backend.conf + sed -i 's/listen 3000 ssl;/listen 3000 ssl proxy_protocol;/' /opt/frontend/https_backend.conf + sed -i '/access_log.*main/a\ set_real_ip_from 0.0.0.0/0;' /opt/frontend/nginx.conf + sed -i '/access_log.*main/a\ real_ip_header proxy_protocol;' /opt/frontend/nginx.conf + echo "Nginx configuration file updated." +} + +mkdir -p /var/log/deepdata/frontend +touch /var/log/deepdata/frontend/access.log /var/log/deepdata/frontend/error.log +chown -R nginx:nginx /var/log/deepdata/frontend 2>/dev/null || true +/usr/local/bin/rotate-nginx-logs.sh & + +if [ -f "/cert/server.pem" ]; then + cp /cert/server.pem /etc/nginx/cert/server.pem + chown nginx:nginx /etc/nginx/cert/server.pem +fi + +if [ -f "/cert/server.key" ]; then + # Private key MUST be encrypted. Plaintext keys are not accepted. + if grep -q "ENCRYPTED" /cert/server.key 2>/dev/null; then + if [ -z "$CERT_PASS" ]; then + echo "ERROR: CERT_PASS is required to decrypt the private key" + exit 1 + fi + echo "$CERT_PASS" | openssl pkey -in /cert/server.key -out /etc/nginx/cert/server.key -passin stdin + chmod 600 /etc/nginx/cert/server.key + else + echo "ERROR: /cert/server.key is not encrypted. Please encrypt it before deployment." + exit 1 + fi + chown nginx:nginx /etc/nginx/cert/server.key +fi + +set_proxy_protocol +cp /opt/frontend/nginx.conf /etc/nginx/nginx.conf +if [ -f "/etc/nginx/cert/server.pem" ]; then + cp /opt/frontend/https_backend.conf /etc/nginx/conf.d/default.conf + cp /opt/frontend/routes.inc /etc/nginx/conf.d/routes.inc + echo "Switching to HTTPS config" +else + cp /opt/frontend/http_backend.conf /etc/nginx/conf.d/default.conf + cp /opt/frontend/routes.inc /etc/nginx/conf.d/routes.inc + + if [ -n "$DOMAIN" ]; then + cron + certbot --nginx "-d ${DOMAIN//,/ -d }" + echo "Switching to HTTPS config, Domain: $DOMAIN" + echo "0 0 1 * * root /usr/local/bin/certbot renew --quiet" | tee /etc/cron.d/certbot-renew + chmod 0644 /etc/cron.d/certbot-renew + else + echo "Switching to HTTP config" + fi +fi + +exec nginx -g "daemon off;" diff --git a/scripts/images/gateway/start.sh b/scripts/images/gateway/start.sh index 589c1ae..5f8f4a7 100644 --- a/scripts/images/gateway/start.sh +++ b/scripts/images/gateway/start.sh @@ -1,6 +1,6 @@ -#!/bin/bash - -set -e - -echo "Starting main application..." +#!/bin/bash + +set -e + +echo "Starting main application..." exec "$@" \ No newline at end of file diff --git a/scripts/images/runtime-ray/start.sh b/scripts/images/runtime-ray/start.sh index fe0a383..4e62321 100644 --- a/scripts/images/runtime-ray/start.sh +++ b/scripts/images/runtime-ray/start.sh @@ -1,35 +1,35 @@ -#!/bin/bash -set -e - -# 1. 初始化环境 (原有的逻辑:注入用户自定义算子) -if [ -d "/opt/runtime/user" ]; then - cp -r /opt/runtime/user/* /opt/runtime/deepdata/ops/user 2>/dev/null || true -fi - -# 2. 根据 RAY_MODE 环境变量决定启动模式 -if [ "${RAY_MODE}" = "head" ]; then - echo "🚀 Starting Ray Head..." - # ✅ 修复 Head 分支: - # Ray 2.x 已废弃 CLI 参数 --redis-address,改为自动读取环境变量 RAY_REDIS_ADDRESS。 - # 只要确保 docker-compose.yml 中配置了 RAY_REDIS_ADDRESS 环境变量即可。 - exec ray start --head \ - --port=${RAY_HEAD_PORT:-6379} \ - --redis-password=${RAY_REDIS_PASSWORD} \ - --dashboard-host=0.0.0.0 \ - --block - -elif [ "${RAY_MODE}" = "worker" ]; then - echo "🚀 Starting Ray Worker connecting to ${RAY_HEAD_ADDRESS}..." - # ✅ 修复 Worker 分支: - # Ray 2.x 移除了 --worker 参数。只要不传 --head,默认即为 Worker 模式。 - exec ray start \ - --address=${RAY_HEAD_ADDRESS} \ - --redis-password=${RAY_REDIS_PASSWORD} \ - --num-cpus=${NUM_CPUS:-4} \ - --block - -else - echo "🔧 Starting Runtime (Legacy Mode / Job Execution)..." - # 默认行为:执行 docker-compose.yml command 传入的指令,或 Ray Job 的入口点 - exec "$@" -fi +#!/bin/bash +set -e + +# 1. 初始化环境 (原有的逻辑:注入用户自定义算子) +if [ -d "/opt/runtime/user" ]; then + cp -r /opt/runtime/user/* /opt/runtime/deepdata/ops/user 2>/dev/null || true +fi + +# 2. 根据 RAY_MODE 环境变量决定启动模式 +if [ "${RAY_MODE}" = "head" ]; then + echo "🚀 Starting Ray Head..." + # ✅ 修复 Head 分支: + # Ray 2.x 已废弃 CLI 参数 --redis-address,改为自动读取环境变量 RAY_REDIS_ADDRESS。 + # 只要确保 docker-compose.yml 中配置了 RAY_REDIS_ADDRESS 环境变量即可。 + exec ray start --head \ + --port=${RAY_HEAD_PORT:-6379} \ + --redis-password=${RAY_REDIS_PASSWORD} \ + --dashboard-host=0.0.0.0 \ + --block + +elif [ "${RAY_MODE}" = "worker" ]; then + echo "🚀 Starting Ray Worker connecting to ${RAY_HEAD_ADDRESS}..." + # ✅ 修复 Worker 分支: + # Ray 2.x 移除了 --worker 参数。只要不传 --head,默认即为 Worker 模式。 + exec ray start \ + --address=${RAY_HEAD_ADDRESS} \ + --redis-password=${RAY_REDIS_PASSWORD} \ + --num-cpus=${NUM_CPUS:-4} \ + --block + +else + echo "🔧 Starting Runtime (Legacy Mode / Job Execution)..." + # 默认行为:执行 docker-compose.yml command 传入的指令,或 Ray Job 的入口点 + exec "$@" +fi diff --git a/scripts/images/runtime/start.sh b/scripts/images/runtime/start.sh index 3866682..856707c 100644 --- a/scripts/images/runtime/start.sh +++ b/scripts/images/runtime/start.sh @@ -1,12 +1,12 @@ -#!/bin/bash - -set -e - -cp -r /opt/runtime/user/* /opt/runtime/deepdata/ops/user - -if [ "${LOG_ROTATION_ENABLED:-true}" = "true" ]; then - LOG_ROTATION_PATH="${LOG_ROTATION_PATH:-/tmp/ray}" /usr/local/bin/log-rotate-copytruncate.sh "${LOG_ROTATION_PATH:-/tmp/ray}" & -fi - -echo "Starting main application..." -exec "$@" +#!/bin/bash + +set -e + +cp -r /opt/runtime/user/* /opt/runtime/deepdata/ops/user + +if [ "${LOG_ROTATION_ENABLED:-true}" = "true" ]; then + LOG_ROTATION_PATH="${LOG_ROTATION_PATH:-/tmp/ray}" /usr/local/bin/log-rotate-copytruncate.sh "${LOG_ROTATION_PATH:-/tmp/ray}" & +fi + +echo "Starting main application..." +exec "$@" diff --git a/scripts/k8s/collect-secrets.sh b/scripts/k8s/collect-secrets.sh index f358bf7..483871f 100644 --- a/scripts/k8s/collect-secrets.sh +++ b/scripts/k8s/collect-secrets.sh @@ -1,248 +1,248 @@ -#!/bin/bash -### Collect secrets for DeepData installation. -### -### Usage: -### scripts/k8s/collect-secrets.sh [--component deepdata|milvus] [--namespace ] -### -### Modes (auto-detected): -### 1. Sealed-secrets mode: controller running + kubeseal available → encrypt → apply SealedSecret -### 2. Plain mode: no controller → Helm creates Secret with base64 (dev mode) -### -### Password priority: .env file values > interactive prompt > auto-generated -### -### Output: writes shell script variables to stdout (eval by caller) - -# NOTE: NOT using 'set -e' because interactive prompts (read) can fail -# when stdin is not a TTY. We validate critical values explicitly instead. - -COMPONENT="${1:-deepdata}" -NAMESPACE="${NAMESPACE:-deepdata}" -ENV_FILE="${ENV_FILE:-.env}" - -# Parse --component and --namespace from args if passed differently -while [[ "$#" -gt 0 ]]; do - case $1 in - --component) COMPONENT="$2"; shift 2 ;; - --namespace|-n) NAMESPACE="$2"; shift 2 ;; - *) shift ;; - esac -done - -TMP_DIR=$(mktemp -d) -trap "rm -rf $TMP_DIR" EXIT - -# ========== Detect Mode ========== -detect_sealed_secrets() { - kubectl get deployment -n "$NAMESPACE" sealed-secrets >/dev/null 2>&1 && return 0 - kubectl get deployment -n kube-system sealed-secrets >/dev/null 2>&1 && return 0 - kubectl get pod -n "$NAMESPACE" -l app.kubernetes.io/name=sealed-secrets --no-headers 2>/dev/null | grep -q Running && return 0 - return 1 -} - -KUBESEAL="$(command -v kubeseal 2>/dev/null || echo "")" -[ -z "$KUBESEAL" ] && KUBESEAL="$HOME/bin/kubeseal" -[ ! -x "$KUBESEAL" ] && [ -x "./tools/bin/kubeseal" ] && KUBESEAL="./tools/bin/kubeseal" -[ ! -x "$KUBESEAL" ] && [ -x "../tools/bin/kubeseal" ] && KUBESEAL="../tools/bin/kubeseal" - -# Auto-download kubeseal if controller exists but binary is missing -if detect_sealed_secrets && [ ! -x "$KUBESEAL" ]; then - echo "[INFO] Sealed Secrets controller detected, but kubeseal not found. Downloading..." >&2 - KUBESEAL_URL="https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.2/kubeseal-0.27.2-$(uname -s | tr '[:upper:]' '[:lower:]')-$(dpkg --print-architecture 2>/dev/null || uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" - KUBESEAL_DIR="$HOME/bin" - mkdir -p "$KUBESEAL_DIR" - curl -sSL "$KUBESEAL_URL" -o "$KUBESEAL_DIR/kubeseal" && chmod +x "$KUBESEAL_DIR/kubeseal" - KUBESEAL="$KUBESEAL_DIR/kubeseal" - echo "[INFO] Installed kubeseal to $KUBESEAL" >&2 -fi - -if detect_sealed_secrets && [ -x "$KUBESEAL" ]; then - MODE="sealed" -else - MODE="plain" -fi - -echo "# DeepData secrets collection — component: $COMPONENT, mode: $MODE" >&2 - -# ========== Helper ========== -random_hex() { - openssl rand -hex 16 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(16))" -} - -seal_secret() { - local name="$1" output="$2" - shift 2 - local raw="${TMP_DIR}/${name}-raw.yaml" - { - echo "apiVersion: v1" - echo "kind: Secret" - echo "metadata:" - echo " name: ${name}" - echo " namespace: ${NAMESPACE}" - echo "type: Opaque" - echo "stringData:" - for pair in "$@"; do - key="${pair%%=*}" - value="${pair#*=}" - echo " ${key}: \"${value}\"" - done - } > "$raw" - "$KUBESEAL" --controller-name=sealed-secrets --namespace="${NAMESPACE}" -o yaml -f "$raw" > "$output" - echo "[INFO] Created SealedSecret: $output" >&2 -} - -# ========== Load .env (targeted: only reads known secret keys) ========== -if [ -f "$ENV_FILE" ]; then - echo "[INFO] Loading secrets from ${ENV_FILE}..." >&2 - - # Read .env line by line, only extract vars matching our known keys. - # Ignores comments, blank lines, and unrelated vars like pgsql_host/shell vars. - while IFS='=' read -r key value; do - # Skip blank lines and comments - [ -z "$key" ] && continue - case "$key" in - \#*) continue ;; - esac - # Strip leading/trailing whitespace from key - key="$(echo "$key" | xargs)" - - case "$key" in - DB_PASSWORD|CERT_PASS|DOMAIN|HOME_PAGE_URL|JWT_SECRET|\ - LABEL_STUDIO_PASSWORD|LABEL_STUDIO_USER_TOKEN|POSTGRE_PASSWORD|\ - MINIO_ACCESS_KEY|MINIO_SECRET_KEY) - val="$value" - case "$val" in - \'*|\"*) val="${val:1}" ;; - esac - case "$val" in - *\'|*\") val="${val:0:-1}" ;; - esac - printf -v "$key" '%s' "$val" - ;; - esac - done < "$ENV_FILE" - - [ -n "$DB_PASSWORD" ] && echo "[INFO] Loaded DB_PASSWORD from $ENV_FILE" >&2 - [ -n "$CERT_PASS" ] && echo "[INFO] Loaded CERT_PASS from $ENV_FILE" >&2 - [ -n "$JWT_SECRET" ] && echo "[INFO] Loaded JWT_SECRET from $ENV_FILE" >&2 -else - echo "[WARN] No .env file found at $(pwd)/${ENV_FILE} — will prompt for secrets" >&2 -fi - -# ========== DeepData ========== -if [ "$COMPONENT" = "deepdata" ]; then - # Collect - [ -z "$DB_PASSWORD" ] && read -rsp "Enter DB_PASSWORD: " DB_PASSWORD && echo "" >&2 - [ -z "$JWT_SECRET" ] && JWT_SECRET=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(32))") && echo "[INFO] Auto-generated JWT_SECRET" >&2 - [ -z "$DOMAIN" ] && read -rp "Enter DOMAIN (enter to skip): " DOMAIN >&2 - [ -z "$CERT_PASS" ] && read -rsp "Enter CERT_PASS (enter to skip): " CERT_PASS && echo "" >&2 - HOME_PAGE_URL="${HOME_PAGE_URL:-/data/management}" - [ -z "$LABEL_STUDIO_PASSWORD" ] && read -rsp "Enter LABEL_STUDIO_PASSWORD (enter to skip): " LABEL_STUDIO_PASSWORD && echo "" >&2 - [ -z "$LABEL_STUDIO_USER_TOKEN" ] && LABEL_STUDIO_USER_TOKEN=$(random_hex)$(random_hex) && echo "[INFO] Auto-generated LABEL_STUDIO_USER_TOKEN" >&2 - - # ===== MANDATORY CHECK: DB_PASSWORD must not be empty ===== - if [ -z "$DB_PASSWORD" ]; then - echo "[FATAL] DB_PASSWORD is empty! Cannot install without a database password." >&2 - echo "[FATAL] Set DB_PASSWORD in .env or ensure interactive prompts work." >&2 - echo "SECRETS_CREATE=SKIP" - echo "HELM_VALUES_FILE=" - exit 0 - fi - - if [ "$MODE" = "sealed" ]; then - # Clean up any old stale Secret (prevents Helm conflict) - if kubectl get secret deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1; then - echo "[INFO] Removing old deepdata-conf Secret..." >&2 - kubectl delete secret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 - fi - # Clean up old SealedSecret (apply is idempotent, but belt-and-suspenders) - kubectl delete sealedsecret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true - - # DeepData-conf - seal_secret "deepdata-conf" "${TMP_DIR}/deepdata-sealed.yaml" \ - "DB_PASSWORD=${DB_PASSWORD}" \ - "CERT_PASS=${CERT_PASS}" \ - "DOMAIN=${DOMAIN}" \ - "HOME_PAGE_URL=${HOME_PAGE_URL}" \ - "JWT_SECRET=${JWT_SECRET}" \ - "LABEL_STUDIO_PASSWORD=${LABEL_STUDIO_PASSWORD}" \ - "LABEL_STUDIO_USER_TOKEN=${LABEL_STUDIO_USER_TOKEN}" - kubectl apply -f "${TMP_DIR}/deepdata-sealed.yaml" -n "$NAMESPACE" >/dev/null - # Add Helm ownership labels so 'helm install --force' accepts the Secret - kubectl wait --for=jsonpath='{.data.DB_PASSWORD}' --timeout=30s secret/deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1 || true - kubectl annotate secret deepdata-conf -n "$NAMESPACE" \ - meta.helm.sh/release-name=deepdata \ - meta.helm.sh/release-namespace="${NAMESPACE}" \ - --overwrite >/dev/null 2>&1 - kubectl label secret deepdata-conf -n "$NAMESPACE" \ - app.kubernetes.io/managed-by=Helm \ - --overwrite >/dev/null 2>&1 - - # Label Studio - if [ -n "$LABEL_STUDIO_PASSWORD" ]; then - POSTGRE_PASSWORD="${POSTGRE_PASSWORD:-$DB_PASSWORD}" - seal_secret "label-studio-env" "${TMP_DIR}/label-studio-sealed.yaml" \ - "POSTGRE_PASSWORD=${POSTGRE_PASSWORD}" \ - "LABEL_STUDIO_PASSWORD=${LABEL_STUDIO_PASSWORD}" \ - "LABEL_STUDIO_USER_TOKEN=${LABEL_STUDIO_USER_TOKEN}" - kubectl apply -f "${TMP_DIR}/label-studio-sealed.yaml" -n "$NAMESPACE" >/dev/null - fi - - echo "SECRETS_CREATE=false" - echo "HELM_VALUES_FILE=" - else - # Plain — delete any old SealedSecret and stale Secret first - kubectl delete sealedsecret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true - if kubectl get secret deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1; then - echo "[INFO] Removing old deepdata-conf Secret..." >&2 - kubectl delete secret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 - fi - - # Write extra values to /tmp (survives script exit for Helm to read) - values_file="/tmp/deepdata-secret-values-$$.yaml" - cat > "$values_file" <&2 - echo "SECRETS_CREATE=true" - echo "HELM_VALUES_FILE=${values_file}" - fi - -# ========== Milvus ========== -elif [ "$COMPONENT" = "milvus" ]; then - # Only needed if secret doesn't already exist - if kubectl get secret milvus-minio-secret -n "$NAMESPACE" >/dev/null 2>&1; then - echo "# milvus-minio-secret already exists — skipping" >&2 - exit 0 - fi - - [ -z "$MINIO_ACCESS_KEY" ] && MINIO_ACCESS_KEY=$(random_hex) && echo "[INFO] Auto-generated MINIO_ACCESS_KEY" >&2 - [ -z "$MINIO_SECRET_KEY" ] && MINIO_SECRET_KEY=$(random_hex)$(random_hex) && echo "[INFO] Auto-generated MINIO_SECRET_KEY" >&2 - - if [ "$MODE" = "sealed" ]; then - seal_secret "milvus-minio-secret" "${TMP_DIR}/milvus-sealed.yaml" \ - "accesskey=${MINIO_ACCESS_KEY}" \ - "secretkey=${MINIO_SECRET_KEY}" - kubectl apply -f "${TMP_DIR}/milvus-sealed.yaml" -n "$NAMESPACE" >/dev/null - kubectl wait --for=jsonpath='{.data.accesskey}' --timeout=30s secret/milvus-minio-secret -n "$NAMESPACE" >/dev/null 2>&1 || true - kubectl annotate secret milvus-minio-secret -n "$NAMESPACE" \ - meta.helm.sh/release-name=milvus-minio \ - meta.helm.sh/release-namespace="${NAMESPACE}" \ - --overwrite >/dev/null 2>&1 - else - kubectl create secret generic milvus-minio-secret \ - --from-literal=accesskey="$MINIO_ACCESS_KEY" \ - --from-literal=secretkey="$MINIO_SECRET_KEY" \ - -n "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null - echo "[INFO] Created milvus-minio-secret (plain)" >&2 - fi -fi +#!/bin/bash +### Collect secrets for DeepData installation. +### +### Usage: +### scripts/k8s/collect-secrets.sh [--component deepdata|milvus] [--namespace ] +### +### Modes (auto-detected): +### 1. Sealed-secrets mode: controller running + kubeseal available → encrypt → apply SealedSecret +### 2. Plain mode: no controller → Helm creates Secret with base64 (dev mode) +### +### Password priority: .env file values > interactive prompt > auto-generated +### +### Output: writes shell script variables to stdout (eval by caller) + +# NOTE: NOT using 'set -e' because interactive prompts (read) can fail +# when stdin is not a TTY. We validate critical values explicitly instead. + +COMPONENT="${1:-deepdata}" +NAMESPACE="${NAMESPACE:-deepdata}" +ENV_FILE="${ENV_FILE:-.env}" + +# Parse --component and --namespace from args if passed differently +while [[ "$#" -gt 0 ]]; do + case $1 in + --component) COMPONENT="$2"; shift 2 ;; + --namespace|-n) NAMESPACE="$2"; shift 2 ;; + *) shift ;; + esac +done + +TMP_DIR=$(mktemp -d) +trap "rm -rf $TMP_DIR" EXIT + +# ========== Detect Mode ========== +detect_sealed_secrets() { + kubectl get deployment -n "$NAMESPACE" sealed-secrets >/dev/null 2>&1 && return 0 + kubectl get deployment -n kube-system sealed-secrets >/dev/null 2>&1 && return 0 + kubectl get pod -n "$NAMESPACE" -l app.kubernetes.io/name=sealed-secrets --no-headers 2>/dev/null | grep -q Running && return 0 + return 1 +} + +KUBESEAL="$(command -v kubeseal 2>/dev/null || echo "")" +[ -z "$KUBESEAL" ] && KUBESEAL="$HOME/bin/kubeseal" +[ ! -x "$KUBESEAL" ] && [ -x "./tools/bin/kubeseal" ] && KUBESEAL="./tools/bin/kubeseal" +[ ! -x "$KUBESEAL" ] && [ -x "../tools/bin/kubeseal" ] && KUBESEAL="../tools/bin/kubeseal" + +# Auto-download kubeseal if controller exists but binary is missing +if detect_sealed_secrets && [ ! -x "$KUBESEAL" ]; then + echo "[INFO] Sealed Secrets controller detected, but kubeseal not found. Downloading..." >&2 + KUBESEAL_URL="https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.2/kubeseal-0.27.2-$(uname -s | tr '[:upper:]' '[:lower:]')-$(dpkg --print-architecture 2>/dev/null || uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" + KUBESEAL_DIR="$HOME/bin" + mkdir -p "$KUBESEAL_DIR" + curl -sSL "$KUBESEAL_URL" -o "$KUBESEAL_DIR/kubeseal" && chmod +x "$KUBESEAL_DIR/kubeseal" + KUBESEAL="$KUBESEAL_DIR/kubeseal" + echo "[INFO] Installed kubeseal to $KUBESEAL" >&2 +fi + +if detect_sealed_secrets && [ -x "$KUBESEAL" ]; then + MODE="sealed" +else + MODE="plain" +fi + +echo "# DeepData secrets collection — component: $COMPONENT, mode: $MODE" >&2 + +# ========== Helper ========== +random_hex() { + openssl rand -hex 16 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(16))" +} + +seal_secret() { + local name="$1" output="$2" + shift 2 + local raw="${TMP_DIR}/${name}-raw.yaml" + { + echo "apiVersion: v1" + echo "kind: Secret" + echo "metadata:" + echo " name: ${name}" + echo " namespace: ${NAMESPACE}" + echo "type: Opaque" + echo "stringData:" + for pair in "$@"; do + key="${pair%%=*}" + value="${pair#*=}" + echo " ${key}: \"${value}\"" + done + } > "$raw" + "$KUBESEAL" --controller-name=sealed-secrets --namespace="${NAMESPACE}" -o yaml -f "$raw" > "$output" + echo "[INFO] Created SealedSecret: $output" >&2 +} + +# ========== Load .env (targeted: only reads known secret keys) ========== +if [ -f "$ENV_FILE" ]; then + echo "[INFO] Loading secrets from ${ENV_FILE}..." >&2 + + # Read .env line by line, only extract vars matching our known keys. + # Ignores comments, blank lines, and unrelated vars like pgsql_host/shell vars. + while IFS='=' read -r key value; do + # Skip blank lines and comments + [ -z "$key" ] && continue + case "$key" in + \#*) continue ;; + esac + # Strip leading/trailing whitespace from key + key="$(echo "$key" | xargs)" + + case "$key" in + DB_PASSWORD|CERT_PASS|DOMAIN|HOME_PAGE_URL|JWT_SECRET|\ + LABEL_STUDIO_PASSWORD|LABEL_STUDIO_USER_TOKEN|POSTGRE_PASSWORD|\ + MINIO_ACCESS_KEY|MINIO_SECRET_KEY) + val="$value" + case "$val" in + \'*|\"*) val="${val:1}" ;; + esac + case "$val" in + *\'|*\") val="${val:0:-1}" ;; + esac + printf -v "$key" '%s' "$val" + ;; + esac + done < "$ENV_FILE" + + [ -n "$DB_PASSWORD" ] && echo "[INFO] Loaded DB_PASSWORD from $ENV_FILE" >&2 + [ -n "$CERT_PASS" ] && echo "[INFO] Loaded CERT_PASS from $ENV_FILE" >&2 + [ -n "$JWT_SECRET" ] && echo "[INFO] Loaded JWT_SECRET from $ENV_FILE" >&2 +else + echo "[WARN] No .env file found at $(pwd)/${ENV_FILE} — will prompt for secrets" >&2 +fi + +# ========== DeepData ========== +if [ "$COMPONENT" = "deepdata" ]; then + # Collect + [ -z "$DB_PASSWORD" ] && read -rsp "Enter DB_PASSWORD: " DB_PASSWORD && echo "" >&2 + [ -z "$JWT_SECRET" ] && JWT_SECRET=$(openssl rand -hex 32 2>/dev/null || python3 -c "import secrets;print(secrets.token_hex(32))") && echo "[INFO] Auto-generated JWT_SECRET" >&2 + [ -z "$DOMAIN" ] && read -rp "Enter DOMAIN (enter to skip): " DOMAIN >&2 + [ -z "$CERT_PASS" ] && read -rsp "Enter CERT_PASS (enter to skip): " CERT_PASS && echo "" >&2 + HOME_PAGE_URL="${HOME_PAGE_URL:-/data/management}" + [ -z "$LABEL_STUDIO_PASSWORD" ] && read -rsp "Enter LABEL_STUDIO_PASSWORD (enter to skip): " LABEL_STUDIO_PASSWORD && echo "" >&2 + [ -z "$LABEL_STUDIO_USER_TOKEN" ] && LABEL_STUDIO_USER_TOKEN=$(random_hex)$(random_hex) && echo "[INFO] Auto-generated LABEL_STUDIO_USER_TOKEN" >&2 + + # ===== MANDATORY CHECK: DB_PASSWORD must not be empty ===== + if [ -z "$DB_PASSWORD" ]; then + echo "[FATAL] DB_PASSWORD is empty! Cannot install without a database password." >&2 + echo "[FATAL] Set DB_PASSWORD in .env or ensure interactive prompts work." >&2 + echo "SECRETS_CREATE=SKIP" + echo "HELM_VALUES_FILE=" + exit 0 + fi + + if [ "$MODE" = "sealed" ]; then + # Clean up any old stale Secret (prevents Helm conflict) + if kubectl get secret deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1; then + echo "[INFO] Removing old deepdata-conf Secret..." >&2 + kubectl delete secret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 + fi + # Clean up old SealedSecret (apply is idempotent, but belt-and-suspenders) + kubectl delete sealedsecret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true + + # DeepData-conf + seal_secret "deepdata-conf" "${TMP_DIR}/deepdata-sealed.yaml" \ + "DB_PASSWORD=${DB_PASSWORD}" \ + "CERT_PASS=${CERT_PASS}" \ + "DOMAIN=${DOMAIN}" \ + "HOME_PAGE_URL=${HOME_PAGE_URL}" \ + "JWT_SECRET=${JWT_SECRET}" \ + "LABEL_STUDIO_PASSWORD=${LABEL_STUDIO_PASSWORD}" \ + "LABEL_STUDIO_USER_TOKEN=${LABEL_STUDIO_USER_TOKEN}" + kubectl apply -f "${TMP_DIR}/deepdata-sealed.yaml" -n "$NAMESPACE" >/dev/null + # Add Helm ownership labels so 'helm install --force' accepts the Secret + kubectl wait --for=jsonpath='{.data.DB_PASSWORD}' --timeout=30s secret/deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1 || true + kubectl annotate secret deepdata-conf -n "$NAMESPACE" \ + meta.helm.sh/release-name=deepdata \ + meta.helm.sh/release-namespace="${NAMESPACE}" \ + --overwrite >/dev/null 2>&1 + kubectl label secret deepdata-conf -n "$NAMESPACE" \ + app.kubernetes.io/managed-by=Helm \ + --overwrite >/dev/null 2>&1 + + # Label Studio + if [ -n "$LABEL_STUDIO_PASSWORD" ]; then + POSTGRE_PASSWORD="${POSTGRE_PASSWORD:-$DB_PASSWORD}" + seal_secret "label-studio-env" "${TMP_DIR}/label-studio-sealed.yaml" \ + "POSTGRE_PASSWORD=${POSTGRE_PASSWORD}" \ + "LABEL_STUDIO_PASSWORD=${LABEL_STUDIO_PASSWORD}" \ + "LABEL_STUDIO_USER_TOKEN=${LABEL_STUDIO_USER_TOKEN}" + kubectl apply -f "${TMP_DIR}/label-studio-sealed.yaml" -n "$NAMESPACE" >/dev/null + fi + + echo "SECRETS_CREATE=false" + echo "HELM_VALUES_FILE=" + else + # Plain — delete any old SealedSecret and stale Secret first + kubectl delete sealedsecret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true + if kubectl get secret deepdata-conf -n "$NAMESPACE" >/dev/null 2>&1; then + echo "[INFO] Removing old deepdata-conf Secret..." >&2 + kubectl delete secret deepdata-conf -n "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 + fi + + # Write extra values to /tmp (survives script exit for Helm to read) + values_file="/tmp/deepdata-secret-values-$$.yaml" + cat > "$values_file" <&2 + echo "SECRETS_CREATE=true" + echo "HELM_VALUES_FILE=${values_file}" + fi + +# ========== Milvus ========== +elif [ "$COMPONENT" = "milvus" ]; then + # Only needed if secret doesn't already exist + if kubectl get secret milvus-minio-secret -n "$NAMESPACE" >/dev/null 2>&1; then + echo "# milvus-minio-secret already exists — skipping" >&2 + exit 0 + fi + + [ -z "$MINIO_ACCESS_KEY" ] && MINIO_ACCESS_KEY=$(random_hex) && echo "[INFO] Auto-generated MINIO_ACCESS_KEY" >&2 + [ -z "$MINIO_SECRET_KEY" ] && MINIO_SECRET_KEY=$(random_hex)$(random_hex) && echo "[INFO] Auto-generated MINIO_SECRET_KEY" >&2 + + if [ "$MODE" = "sealed" ]; then + seal_secret "milvus-minio-secret" "${TMP_DIR}/milvus-sealed.yaml" \ + "accesskey=${MINIO_ACCESS_KEY}" \ + "secretkey=${MINIO_SECRET_KEY}" + kubectl apply -f "${TMP_DIR}/milvus-sealed.yaml" -n "$NAMESPACE" >/dev/null + kubectl wait --for=jsonpath='{.data.accesskey}' --timeout=30s secret/milvus-minio-secret -n "$NAMESPACE" >/dev/null 2>&1 || true + kubectl annotate secret milvus-minio-secret -n "$NAMESPACE" \ + meta.helm.sh/release-name=milvus-minio \ + meta.helm.sh/release-namespace="${NAMESPACE}" \ + --overwrite >/dev/null 2>&1 + else + kubectl create secret generic milvus-minio-secret \ + --from-literal=accesskey="$MINIO_ACCESS_KEY" \ + --from-literal=secretkey="$MINIO_SECRET_KEY" \ + -n "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + echo "[INFO] Created milvus-minio-secret (plain)" >&2 + fi +fi diff --git a/scripts/k8s/node-cleanup.sh b/scripts/k8s/node-cleanup.sh index b9cbecb..2eb2d3d 100644 --- a/scripts/k8s/node-cleanup.sh +++ b/scripts/k8s/node-cleanup.sh @@ -1,167 +1,167 @@ -#!/bin/bash -# -# DeepData Node Cleanup Script -# Remove labels and taints from nodes that were configured for DeepData deployment -# -# Usage: ./node-cleanup.sh [--dry-run] [--nodes NODE1,NODE2] -# - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Default values -DRY_RUN=false -NAMESPACE="deepdata" -LABEL_KEY="node-role.kubernetes.io/deepdata" -LABEL_VALUE="true" -TAINT_EFFECT="NoSchedule" - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - --namespace) - NAMESPACE="$2" - shift 2 - ;; - --dry-run) - DRY_RUN=true - shift - ;; - --nodes) - PROVIDED_NODES="$2" - shift 2 - ;; - --label-key) - LABEL_KEY="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -echo -e "${BLUE}=====================================${NC}" -echo -e "${BLUE} DeepData Node Cleanup${NC}" -echo -e "${BLUE}=====================================${NC}" -echo "" - -# Check if kubectl is available -if ! command -v kubectl &> /dev/null; then - echo -e "${RED}Error: kubectl is not installed${NC}" - exit 1 -fi - -# Check if connected to cluster -if ! kubectl cluster-info &> /dev/null; then - echo -e "${RED}Error: Cannot connect to Kubernetes cluster${NC}" - exit 1 -fi - -# Determine nodes to clean up -if [ "$PROVIDED_NODES" != "" ]; then - # Use provided nodes - IFS=',' read -ra SELECTED_NODES <<< "$PROVIDED_NODES" -else - # Find nodes with the deepdata label directly from Kubernetes - echo -e "${YELLOW}Finding nodes with $LABEL_KEY=$LABEL_VALUE label...${NC}" - NODES=$(kubectl get nodes -l "$LABEL_KEY=$LABEL_VALUE" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') - - if [ -z "$NODES" ]; then - echo -e "${GREEN}No nodes found with $LABEL_KEY=$LABEL_VALUE label.${NC}" - echo -e "${YELLOW}Cleanup not needed - no nodes were labeled.${NC}" - exit 0 - fi - - SELECTED_NODES=() - while IFS= read -r NODE; do - if [ -n "$NODE" ]; then - SELECTED_NODES+=("$NODE") - fi - done <<< "$NODES" -fi - -if [ ${#SELECTED_NODES[@]} -eq 0 ]; then - echo -e "${YELLOW}No nodes to clean up.${NC}" - exit 0 -fi - -echo -e "${GREEN}Nodes to clean up:${NC}" -for NODE in "${SELECTED_NODES[@]}"; do - echo " - $NODE" -done - -echo "" -echo -e "${BLUE}Summary:${NC}" -echo " Label to remove: $LABEL_KEY" - -# Check if any node has the taint -HAS_TAINTS=false -for NODE in "${SELECTED_NODES[@]}"; do - TAINT_COUNT=$(kubectl get node "$NODE" -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' | grep -c "^${LABEL_KEY}$" || echo "0") - if [ "$TAINT_COUNT" -gt 0 ]; then - HAS_TAINTS=true - break - fi -done - -if [ "$HAS_TAINTS" = true ]; then - echo " Taint to remove: $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT" -fi -echo "" - -read -p "Remove labels and taints? (y/n) [y]: " CONFIRM -CONFIRM=${CONFIRM:-y} - -if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then - echo -e "${YELLOW}Cancelled.${NC}" - exit 0 -fi - -echo "" -echo -e "${GREEN}Removing configuration...${NC}" - -# Remove labels from selected nodes -for NODE in "${SELECTED_NODES[@]}"; do - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] kubectl label node $NODE $LABEL_KEY-" - else - kubectl label node "$NODE" "$LABEL_KEY-" --overwrite - echo -e " ${GREEN}✓${NC} Removed label from $NODE" - fi -done - -# Remove taints (check if node has the taint) -for NODE in "${SELECTED_NODES[@]}"; do - # Check if node has the taint - HAS_TAINT=$(kubectl get node "$NODE" -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' | grep -c "^${LABEL_KEY}$" || echo "0") - - if [ "$HAS_TAINT" -gt 0 ]; then - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] kubectl taint node $NODE $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT-" - else - kubectl taint node "$NODE" "$LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT-" --overwrite || true - echo -e " ${GREEN}✓${NC} Removed taint from $NODE" - fi - else - echo -e " ${YELLOW}○${NC} No taint to remove from $NODE" - fi -done - -echo "" -echo -e "${GREEN}Cleanup complete!${NC}" - -# Clean up Helm args temp file if it exists -if [ -f /tmp/deepdata-helm-args.sh ]; then - rm /tmp/deepdata-helm-args.sh - echo -e "${GREEN}Removed /tmp/deepdata-helm-args.sh${NC}" -fi - -exit 0 +#!/bin/bash +# +# DeepData Node Cleanup Script +# Remove labels and taints from nodes that were configured for DeepData deployment +# +# Usage: ./node-cleanup.sh [--dry-run] [--nodes NODE1,NODE2] +# + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default values +DRY_RUN=false +NAMESPACE="deepdata" +LABEL_KEY="node-role.kubernetes.io/deepdata" +LABEL_VALUE="true" +TAINT_EFFECT="NoSchedule" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --namespace) + NAMESPACE="$2" + shift 2 + ;; + --dry-run) + DRY_RUN=true + shift + ;; + --nodes) + PROVIDED_NODES="$2" + shift 2 + ;; + --label-key) + LABEL_KEY="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +echo -e "${BLUE}=====================================${NC}" +echo -e "${BLUE} DeepData Node Cleanup${NC}" +echo -e "${BLUE}=====================================${NC}" +echo "" + +# Check if kubectl is available +if ! command -v kubectl &> /dev/null; then + echo -e "${RED}Error: kubectl is not installed${NC}" + exit 1 +fi + +# Check if connected to cluster +if ! kubectl cluster-info &> /dev/null; then + echo -e "${RED}Error: Cannot connect to Kubernetes cluster${NC}" + exit 1 +fi + +# Determine nodes to clean up +if [ "$PROVIDED_NODES" != "" ]; then + # Use provided nodes + IFS=',' read -ra SELECTED_NODES <<< "$PROVIDED_NODES" +else + # Find nodes with the deepdata label directly from Kubernetes + echo -e "${YELLOW}Finding nodes with $LABEL_KEY=$LABEL_VALUE label...${NC}" + NODES=$(kubectl get nodes -l "$LABEL_KEY=$LABEL_VALUE" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') + + if [ -z "$NODES" ]; then + echo -e "${GREEN}No nodes found with $LABEL_KEY=$LABEL_VALUE label.${NC}" + echo -e "${YELLOW}Cleanup not needed - no nodes were labeled.${NC}" + exit 0 + fi + + SELECTED_NODES=() + while IFS= read -r NODE; do + if [ -n "$NODE" ]; then + SELECTED_NODES+=("$NODE") + fi + done <<< "$NODES" +fi + +if [ ${#SELECTED_NODES[@]} -eq 0 ]; then + echo -e "${YELLOW}No nodes to clean up.${NC}" + exit 0 +fi + +echo -e "${GREEN}Nodes to clean up:${NC}" +for NODE in "${SELECTED_NODES[@]}"; do + echo " - $NODE" +done + +echo "" +echo -e "${BLUE}Summary:${NC}" +echo " Label to remove: $LABEL_KEY" + +# Check if any node has the taint +HAS_TAINTS=false +for NODE in "${SELECTED_NODES[@]}"; do + TAINT_COUNT=$(kubectl get node "$NODE" -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' | grep -c "^${LABEL_KEY}$" || echo "0") + if [ "$TAINT_COUNT" -gt 0 ]; then + HAS_TAINTS=true + break + fi +done + +if [ "$HAS_TAINTS" = true ]; then + echo " Taint to remove: $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT" +fi +echo "" + +read -p "Remove labels and taints? (y/n) [y]: " CONFIRM +CONFIRM=${CONFIRM:-y} + +if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then + echo -e "${YELLOW}Cancelled.${NC}" + exit 0 +fi + +echo "" +echo -e "${GREEN}Removing configuration...${NC}" + +# Remove labels from selected nodes +for NODE in "${SELECTED_NODES[@]}"; do + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] kubectl label node $NODE $LABEL_KEY-" + else + kubectl label node "$NODE" "$LABEL_KEY-" --overwrite + echo -e " ${GREEN}✓${NC} Removed label from $NODE" + fi +done + +# Remove taints (check if node has the taint) +for NODE in "${SELECTED_NODES[@]}"; do + # Check if node has the taint + HAS_TAINT=$(kubectl get node "$NODE" -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' | grep -c "^${LABEL_KEY}$" || echo "0") + + if [ "$HAS_TAINT" -gt 0 ]; then + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] kubectl taint node $NODE $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT-" + else + kubectl taint node "$NODE" "$LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT-" --overwrite || true + echo -e " ${GREEN}✓${NC} Removed taint from $NODE" + fi + else + echo -e " ${YELLOW}○${NC} No taint to remove from $NODE" + fi +done + +echo "" +echo -e "${GREEN}Cleanup complete!${NC}" + +# Clean up Helm args temp file if it exists +if [ -f /tmp/deepdata-helm-args.sh ]; then + rm /tmp/deepdata-helm-args.sh + echo -e "${GREEN}Removed /tmp/deepdata-helm-args.sh${NC}" +fi + +exit 0 diff --git a/scripts/k8s/node-setup.sh b/scripts/k8s/node-setup.sh index 997a3d8..a3987e8 100644 --- a/scripts/k8s/node-setup.sh +++ b/scripts/k8s/node-setup.sh @@ -1,504 +1,504 @@ -#!/bin/bash -# -# DeepData Node Setup Script -# Interactive script to select nodes with keyboard navigation (↑/↓ or j/k) -# Automatically applies labels and taints for DeepData deployment -# -# Usage: ./node-setup.sh [--dry-run] [--namespace NAMESPACE] [--skip-taint] -# - -set -e - -# ============================================================================ -# Configuration -# ============================================================================ - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# Fixed label and taint values (no prompts needed) -NAMESPACE="deepdata" -LABEL_KEY="node-role.kubernetes.io/deepdata" -LABEL_VALUE="true" -TAINT_EFFECT="NoSchedule" -DRY_RUN=false -SKIP_TAINT=false - -# ============================================================================ -# Argument Parsing -# ============================================================================ - -parse_args() { - while [[ $# -gt 0 ]]; do - case $1 in - --dry-run) - DRY_RUN=true - shift - ;; - --namespace) - NAMESPACE="$2" - shift 2 - ;; - --skip-taint) - SKIP_TAINT=true - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac - done -} - -# ============================================================================ -# Prerequisite Checks -# ============================================================================ - -check_prerequisites() { - if ! command -v kubectl &> /dev/null; then - echo -e "${RED}Error: kubectl is not installed${NC}" - exit 1 - fi - - if ! kubectl cluster-info &> /dev/null; then - echo -e "${RED}Error: Cannot connect to Kubernetes cluster${NC}" - exit 1 - fi -} - -# ============================================================================ -# Node Data Collection -# ============================================================================ - -fetch_nodes() { - echo -e "${YELLOW}Fetching available nodes...${NC}" - - NODES=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') - NODE_COUNT=$(echo "$NODES" | wc -l | tr -d ' ') - - if [ "$NODE_COUNT" -eq 0 ]; then - echo -e "${RED}Error: No nodes found in the cluster${NC}" - exit 1 - fi - - # Build node array with status info - NODE_ARRAY=() - NODE_STATUS=() - NODE_HAS_LABEL=() - - for NODE in $NODES; do - NODE_ARRAY+=("$NODE") - - # Get status - STATUS=$(kubectl get node "$NODE" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') - NODE_STATUS+=("$STATUS") - - # Check if already labeled - CURRENT_LABEL=$(kubectl get node "$NODE" -o jsonpath='{.metadata.labels.node-role\.kubernetes\.io/deepdata}') - if [ "$CURRENT_LABEL" = "true" ]; then - NODE_HAS_LABEL+=("true") - else - NODE_HAS_LABEL+=("false") - fi - done -} - -# ============================================================================ -# Interactive Menu Functions -# ============================================================================ - -# Initialize selection state array -init_selection() { - SELECTED=() - for i in $(seq 1 $NODE_COUNT); do - SELECTED+=("false") - done - CURRENT_INDEX=0 -} - -# Print the interactive menu -print_menu() { - # Clear screen and print header - echo -e "\033[2J\033[H" - echo -e "${BLUE}=====================================${NC}" - echo -e "${BLUE} DeepData Node Setup${NC}" - echo -e "${BLUE}=====================================${NC}" - echo "" - echo -e "${CYAN}Select nodes for DeepData deployment${NC}" - echo "" - - # Print each node with selection marker - for i in $(seq 0 $(($NODE_COUNT - 1))); do - NODE="${NODE_ARRAY[$i]}" - STATUS="${NODE_STATUS[$i]}" - HAS_LABEL="${NODE_HAS_LABEL[$i]}" - IS_SELECTED="${SELECTED[$i]}" - - # Status display - if [ "$STATUS" = "True" ]; then - STATUS_DISPLAY="${GREEN}Ready${NC}" - else - STATUS_DISPLAY="${RED}NotReady${NC}" - fi - - # Label marker - LABEL_MARKER="" - if [ "$HAS_LABEL" = "true" ]; then - LABEL_MARKER=" ${GREEN}[deepdata]${NC}" - fi - - # Selection marker - if [ "$IS_SELECTED" = "true" ]; then - MARKER="${GREEN}[x]${NC}" - else - MARKER="${NC}[ ]${NC}" - fi - - # Highlight current row - if [ "$i" -eq "$CURRENT_INDEX" ]; then - echo -e " ${YELLOW}→${NC} $MARKER ${CYAN}$NODE${NC} ($STATUS_DISPLAY)$LABEL_MARKER" - else - echo -e " $MARKER $NODE ($STATUS_DISPLAY)$LABEL_MARKER" - fi - done - - echo "" - echo -e "${YELLOW}Navigation:${NC} ↑/k: up ↓/j: down ${GREEN}space${NC}: toggle ${GREEN}enter${NC}: confirm ${RED}q${NC}: quit" - echo "" - - # Show current selection count - SELECTED_COUNT=0 - for s in "${SELECTED[@]}"; do - if [ "$s" = "true" ]; then - SELECTED_COUNT=$((SELECTED_COUNT + 1)) - fi - done - echo -e "${BLUE}Selected: ${SELECTED_COUNT}/${NODE_COUNT} nodes${NC}" -} - -# Toggle selection at current index -toggle_selection() { - if [ "${SELECTED[$CURRENT_INDEX]}" = "true" ]; then - SELECTED[$CURRENT_INDEX]="false" - else - SELECTED[$CURRENT_INDEX]="true" - fi -} - -# Move cursor up -move_up() { - if [ "$CURRENT_INDEX" -gt 0 ]; then - CURRENT_INDEX=$((CURRENT_INDEX - 1)) - fi -} - -# Move cursor down -move_down() { - if [ "$CURRENT_INDEX" -lt $(($NODE_COUNT - 1)) ]; then - CURRENT_INDEX=$((CURRENT_INDEX + 1)) - fi -} - -# Get selected nodes list -get_selected_nodes() { - SELECTED_NODES=() - for i in $(seq 0 $(($NODE_COUNT - 1))); do - if [ "${SELECTED[$i]}" = "true" ]; then - SELECTED_NODES+=("${NODE_ARRAY[$i]}") - fi - done -} - -# ============================================================================ -# Keyboard Input Handling -# ============================================================================ - -# Read single keypress (with fallback for non-terminal environments) -read_key() { - # Check if we're in a proper terminal - if [ ! -t 0 ]; then - # Not in terminal - use simple read mode - read -r key - echo "$key" - return - fi - - # Save current terminal settings - old_stty=$(stty -g 2>/dev/null) || return 1 - - # Set terminal to raw mode for single char read - stty raw -echo 2>/dev/null - - # Read key - key=$(dd bs=1 count=1 2>/dev/null) - - # Check for arrow keys (escape sequence) - if [ "$key" = $'\x1b' ]; then - # Read the next two chars for arrow key sequence - read -rs -t0.1 -n2 key2 2>/dev/null || true - key="${key}${key2}" - fi - - # Restore terminal settings BEFORE processing - stty "$old_stty" 2>/dev/null || true - - # In raw mode, Enter produces \r (carriage return), convert to \n for easier matching - if [ "$key" = $'\x0d' ]; then - key=$'\x0a' - fi - - echo "$key" -} - -# Main interactive loop -interactive_selection() { - init_selection - - while true; do - print_menu - - key=$(read_key) - - case "$key" in - # Arrow up or 'k' - $'\x1b[A'|k) - move_up - ;; - # Arrow down or 'j' - $'\x1b[B'|j) - move_down - ;; - # Space - toggle selection - ' ') - toggle_selection - ;; - # Enter - confirm - ''|$'\x0a') - get_selected_nodes - if [ ${#SELECTED_NODES[@]} -eq 0 ]; then - echo -e "${YELLOW}No nodes selected. Please select at least one node.${NC}" - sleep 1 - else - return 0 - fi - ;; - # Q - quit/skip - q|Q) - echo -e "\n${YELLOW}Skipping node setup.${NC}" - echo "" - echo "HELM_NODE_SELECTOR_ARGS=" - echo "HELM_TOLERATIONS_ARGS=" - exit 0 - ;; - esac - done -} - -# ============================================================================ -# Apply Configuration -# ============================================================================ - -apply_labels_and_taints() { - echo "" - echo -e "${GREEN}Applying configuration...${NC}" - echo "" - echo -e "${BLUE}Configuration:${NC}" - echo " Label: ${LABEL_KEY}=${LABEL_VALUE}" - if [ "$SKIP_TAINT" = false ]; then - echo " Taint: ${LABEL_KEY}=${LABEL_VALUE}:${TAINT_EFFECT}" - fi - echo "" - - # Apply labels - for NODE in "${SELECTED_NODES[@]}"; do - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] kubectl label node $NODE $LABEL_KEY=$LABEL_VALUE --overwrite" - else - kubectl label node "$NODE" "$LABEL_KEY=$LABEL_VALUE" --overwrite - echo -e " ${GREEN}✓${NC} Applied label to $NODE" - fi - done - - # Apply taints (unless skipped) - if [ "$SKIP_TAINT" = false ]; then - for NODE in "${SELECTED_NODES[@]}"; do - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] kubectl taint node $NODE $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT --overwrite" - else - kubectl taint node "$NODE" "$LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT" --overwrite - echo -e " ${GREEN}✓${NC} Applied taint to $NODE" - fi - done - fi - - echo "" - echo -e "${GREEN}Configuration complete!${NC}" -} - -# ============================================================================ -# Generate Helm Arguments -# ============================================================================ - -generate_helm_args() { - # Escape dots in label key for Helm (dots are interpreted as nested keys) - LABEL_KEY_ESCAPED=$(echo "$LABEL_KEY" | sed 's/\./\\./g') - - # Use --set-string to force string type (avoids boolean interpretation) - - # Node selector args - HELM_NODE_SELECTOR_ARGS="--set-string global.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" - - SERVICES="backend backend-python database frontend gateway runtime" - for SERVICE in $SERVICES; do - HELM_NODE_SELECTOR_ARGS="$HELM_NODE_SELECTOR_ARGS --set-string ${SERVICE}.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" - done - - # Tolerations args (if taint applied) - if [ "$SKIP_TAINT" = false ]; then - HELM_TOLERATIONS_ARGS="--set-string global.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].effect=${TAINT_EFFECT}" - - # Milvus-specific tolerations (Milvus chart uses direct tolerations, not global.tolerations) - HELM_MILVUS_TOLERATIONS="--set-string tolerations[0].key=${LABEL_KEY}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].operator=Equal" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].value=${LABEL_VALUE}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].effect=${TAINT_EFFECT}" - - # Milvus sub-chart tolerations (etcd, minio don't inherit parent chart tolerations) - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].key=${LABEL_KEY}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].operator=Equal" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].value=${LABEL_VALUE}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].effect=${TAINT_EFFECT}" - - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].key=${LABEL_KEY}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].operator=Equal" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].value=${LABEL_VALUE}" - HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].effect=${TAINT_EFFECT}" - - # Label-studio tolerations (app + pgbouncer) - HELM_LABEL_STUDIO_TOLERATIONS="--set-string tolerations[0].key=${LABEL_KEY}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].operator=Equal" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].value=${LABEL_VALUE}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].effect=${TAINT_EFFECT}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].key=${LABEL_KEY}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].operator=Equal" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].value=${LABEL_VALUE}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].effect=${TAINT_EFFECT}" - HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" - - for SERVICE in $SERVICES; do - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].effect=${TAINT_EFFECT}" - done - - # Ray cluster - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].effect=${TAINT_EFFECT}" - - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].effect=${TAINT_EFFECT}" - - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].effect=${TAINT_EFFECT}" - - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].effect=${TAINT_EFFECT}" - - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].key=${LABEL_KEY}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].operator=Equal" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].value=${LABEL_VALUE}" - HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].effect=${TAINT_EFFECT}" - else - HELM_TOLERATIONS_ARGS="" - HELM_MILVUS_TOLERATIONS="" - HELM_LABEL_STUDIO_TOLERATIONS="" - fi - - # Write Helm args to temp file for Makefile to source - HELM_ARGS_FILE="/tmp/deepdata-helm-args.sh" - cat > "$HELM_ARGS_FILE" < "$HELM_ARGS_FILE" < /dev/null; then + echo -e "${RED}Error: kubectl is not installed${NC}" + exit 1 + fi + + if ! kubectl cluster-info &> /dev/null; then + echo -e "${RED}Error: Cannot connect to Kubernetes cluster${NC}" + exit 1 + fi +} + +# ============================================================================ +# Node Data Collection +# ============================================================================ + +fetch_nodes() { + echo -e "${YELLOW}Fetching available nodes...${NC}" + + NODES=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') + NODE_COUNT=$(echo "$NODES" | wc -l | tr -d ' ') + + if [ "$NODE_COUNT" -eq 0 ]; then + echo -e "${RED}Error: No nodes found in the cluster${NC}" + exit 1 + fi + + # Build node array with status info + NODE_ARRAY=() + NODE_STATUS=() + NODE_HAS_LABEL=() + + for NODE in $NODES; do + NODE_ARRAY+=("$NODE") + + # Get status + STATUS=$(kubectl get node "$NODE" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') + NODE_STATUS+=("$STATUS") + + # Check if already labeled + CURRENT_LABEL=$(kubectl get node "$NODE" -o jsonpath='{.metadata.labels.node-role\.kubernetes\.io/deepdata}') + if [ "$CURRENT_LABEL" = "true" ]; then + NODE_HAS_LABEL+=("true") + else + NODE_HAS_LABEL+=("false") + fi + done +} + +# ============================================================================ +# Interactive Menu Functions +# ============================================================================ + +# Initialize selection state array +init_selection() { + SELECTED=() + for i in $(seq 1 $NODE_COUNT); do + SELECTED+=("false") + done + CURRENT_INDEX=0 +} + +# Print the interactive menu +print_menu() { + # Clear screen and print header + echo -e "\033[2J\033[H" + echo -e "${BLUE}=====================================${NC}" + echo -e "${BLUE} DeepData Node Setup${NC}" + echo -e "${BLUE}=====================================${NC}" + echo "" + echo -e "${CYAN}Select nodes for DeepData deployment${NC}" + echo "" + + # Print each node with selection marker + for i in $(seq 0 $(($NODE_COUNT - 1))); do + NODE="${NODE_ARRAY[$i]}" + STATUS="${NODE_STATUS[$i]}" + HAS_LABEL="${NODE_HAS_LABEL[$i]}" + IS_SELECTED="${SELECTED[$i]}" + + # Status display + if [ "$STATUS" = "True" ]; then + STATUS_DISPLAY="${GREEN}Ready${NC}" + else + STATUS_DISPLAY="${RED}NotReady${NC}" + fi + + # Label marker + LABEL_MARKER="" + if [ "$HAS_LABEL" = "true" ]; then + LABEL_MARKER=" ${GREEN}[deepdata]${NC}" + fi + + # Selection marker + if [ "$IS_SELECTED" = "true" ]; then + MARKER="${GREEN}[x]${NC}" + else + MARKER="${NC}[ ]${NC}" + fi + + # Highlight current row + if [ "$i" -eq "$CURRENT_INDEX" ]; then + echo -e " ${YELLOW}→${NC} $MARKER ${CYAN}$NODE${NC} ($STATUS_DISPLAY)$LABEL_MARKER" + else + echo -e " $MARKER $NODE ($STATUS_DISPLAY)$LABEL_MARKER" + fi + done + + echo "" + echo -e "${YELLOW}Navigation:${NC} ↑/k: up ↓/j: down ${GREEN}space${NC}: toggle ${GREEN}enter${NC}: confirm ${RED}q${NC}: quit" + echo "" + + # Show current selection count + SELECTED_COUNT=0 + for s in "${SELECTED[@]}"; do + if [ "$s" = "true" ]; then + SELECTED_COUNT=$((SELECTED_COUNT + 1)) + fi + done + echo -e "${BLUE}Selected: ${SELECTED_COUNT}/${NODE_COUNT} nodes${NC}" +} + +# Toggle selection at current index +toggle_selection() { + if [ "${SELECTED[$CURRENT_INDEX]}" = "true" ]; then + SELECTED[$CURRENT_INDEX]="false" + else + SELECTED[$CURRENT_INDEX]="true" + fi +} + +# Move cursor up +move_up() { + if [ "$CURRENT_INDEX" -gt 0 ]; then + CURRENT_INDEX=$((CURRENT_INDEX - 1)) + fi +} + +# Move cursor down +move_down() { + if [ "$CURRENT_INDEX" -lt $(($NODE_COUNT - 1)) ]; then + CURRENT_INDEX=$((CURRENT_INDEX + 1)) + fi +} + +# Get selected nodes list +get_selected_nodes() { + SELECTED_NODES=() + for i in $(seq 0 $(($NODE_COUNT - 1))); do + if [ "${SELECTED[$i]}" = "true" ]; then + SELECTED_NODES+=("${NODE_ARRAY[$i]}") + fi + done +} + +# ============================================================================ +# Keyboard Input Handling +# ============================================================================ + +# Read single keypress (with fallback for non-terminal environments) +read_key() { + # Check if we're in a proper terminal + if [ ! -t 0 ]; then + # Not in terminal - use simple read mode + read -r key + echo "$key" + return + fi + + # Save current terminal settings + old_stty=$(stty -g 2>/dev/null) || return 1 + + # Set terminal to raw mode for single char read + stty raw -echo 2>/dev/null + + # Read key + key=$(dd bs=1 count=1 2>/dev/null) + + # Check for arrow keys (escape sequence) + if [ "$key" = $'\x1b' ]; then + # Read the next two chars for arrow key sequence + read -rs -t0.1 -n2 key2 2>/dev/null || true + key="${key}${key2}" + fi + + # Restore terminal settings BEFORE processing + stty "$old_stty" 2>/dev/null || true + + # In raw mode, Enter produces \r (carriage return), convert to \n for easier matching + if [ "$key" = $'\x0d' ]; then + key=$'\x0a' + fi + + echo "$key" +} + +# Main interactive loop +interactive_selection() { + init_selection + + while true; do + print_menu + + key=$(read_key) + + case "$key" in + # Arrow up or 'k' + $'\x1b[A'|k) + move_up + ;; + # Arrow down or 'j' + $'\x1b[B'|j) + move_down + ;; + # Space - toggle selection + ' ') + toggle_selection + ;; + # Enter - confirm + ''|$'\x0a') + get_selected_nodes + if [ ${#SELECTED_NODES[@]} -eq 0 ]; then + echo -e "${YELLOW}No nodes selected. Please select at least one node.${NC}" + sleep 1 + else + return 0 + fi + ;; + # Q - quit/skip + q|Q) + echo -e "\n${YELLOW}Skipping node setup.${NC}" + echo "" + echo "HELM_NODE_SELECTOR_ARGS=" + echo "HELM_TOLERATIONS_ARGS=" + exit 0 + ;; + esac + done +} + +# ============================================================================ +# Apply Configuration +# ============================================================================ + +apply_labels_and_taints() { + echo "" + echo -e "${GREEN}Applying configuration...${NC}" + echo "" + echo -e "${BLUE}Configuration:${NC}" + echo " Label: ${LABEL_KEY}=${LABEL_VALUE}" + if [ "$SKIP_TAINT" = false ]; then + echo " Taint: ${LABEL_KEY}=${LABEL_VALUE}:${TAINT_EFFECT}" + fi + echo "" + + # Apply labels + for NODE in "${SELECTED_NODES[@]}"; do + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] kubectl label node $NODE $LABEL_KEY=$LABEL_VALUE --overwrite" + else + kubectl label node "$NODE" "$LABEL_KEY=$LABEL_VALUE" --overwrite + echo -e " ${GREEN}✓${NC} Applied label to $NODE" + fi + done + + # Apply taints (unless skipped) + if [ "$SKIP_TAINT" = false ]; then + for NODE in "${SELECTED_NODES[@]}"; do + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] kubectl taint node $NODE $LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT --overwrite" + else + kubectl taint node "$NODE" "$LABEL_KEY=$LABEL_VALUE:$TAINT_EFFECT" --overwrite + echo -e " ${GREEN}✓${NC} Applied taint to $NODE" + fi + done + fi + + echo "" + echo -e "${GREEN}Configuration complete!${NC}" +} + +# ============================================================================ +# Generate Helm Arguments +# ============================================================================ + +generate_helm_args() { + # Escape dots in label key for Helm (dots are interpreted as nested keys) + LABEL_KEY_ESCAPED=$(echo "$LABEL_KEY" | sed 's/\./\\./g') + + # Use --set-string to force string type (avoids boolean interpretation) + + # Node selector args + HELM_NODE_SELECTOR_ARGS="--set-string global.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" + + SERVICES="backend backend-python database frontend gateway runtime" + for SERVICE in $SERVICES; do + HELM_NODE_SELECTOR_ARGS="$HELM_NODE_SELECTOR_ARGS --set-string ${SERVICE}.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" + done + + # Tolerations args (if taint applied) + if [ "$SKIP_TAINT" = false ]; then + HELM_TOLERATIONS_ARGS="--set-string global.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string global.tolerations[0].effect=${TAINT_EFFECT}" + + # Milvus-specific tolerations (Milvus chart uses direct tolerations, not global.tolerations) + HELM_MILVUS_TOLERATIONS="--set-string tolerations[0].key=${LABEL_KEY}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].operator=Equal" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].value=${LABEL_VALUE}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string tolerations[0].effect=${TAINT_EFFECT}" + + # Milvus sub-chart tolerations (etcd, minio don't inherit parent chart tolerations) + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].key=${LABEL_KEY}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].operator=Equal" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].value=${LABEL_VALUE}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string etcd.tolerations[0].effect=${TAINT_EFFECT}" + + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].key=${LABEL_KEY}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].operator=Equal" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].value=${LABEL_VALUE}" + HELM_MILVUS_TOLERATIONS="$HELM_MILVUS_TOLERATIONS --set-string minio.tolerations[0].effect=${TAINT_EFFECT}" + + # Label-studio tolerations (app + pgbouncer) + HELM_LABEL_STUDIO_TOLERATIONS="--set-string tolerations[0].key=${LABEL_KEY}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].operator=Equal" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].value=${LABEL_VALUE}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string tolerations[0].effect=${TAINT_EFFECT}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].key=${LABEL_KEY}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].operator=Equal" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].value=${LABEL_VALUE}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.tolerations[0].effect=${TAINT_EFFECT}" + HELM_LABEL_STUDIO_TOLERATIONS="$HELM_LABEL_STUDIO_TOLERATIONS --set-string pgbouncer.nodeSelector.${LABEL_KEY_ESCAPED}=${LABEL_VALUE}" + + for SERVICE in $SERVICES; do + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ${SERVICE}.tolerations[0].effect=${TAINT_EFFECT}" + done + + # Ray cluster + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.head.tolerations[0].effect=${TAINT_EFFECT}" + + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.worker.tolerations[0].effect=${TAINT_EFFECT}" + + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.npuGroup.tolerations[0].effect=${TAINT_EFFECT}" + + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string ray-cluster.additionalWorkerGroups.gpuGroup.tolerations[0].effect=${TAINT_EFFECT}" + + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].key=${LABEL_KEY}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].operator=Equal" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].value=${LABEL_VALUE}" + HELM_TOLERATIONS_ARGS="$HELM_TOLERATIONS_ARGS --set-string kuberay-operator.tolerations[0].effect=${TAINT_EFFECT}" + else + HELM_TOLERATIONS_ARGS="" + HELM_MILVUS_TOLERATIONS="" + HELM_LABEL_STUDIO_TOLERATIONS="" + fi + + # Write Helm args to temp file for Makefile to source + HELM_ARGS_FILE="/tmp/deepdata-helm-args.sh" + cat > "$HELM_ARGS_FILE" < "$HELM_ARGS_FILE" <