diff --git a/client/Chart.yaml b/client/Chart.yaml index 6ffa41c..1fb00f1 100644 --- a/client/Chart.yaml +++ b/client/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: client description: A unified Helm chart for tracebloc on AKS, EKS, bare-metal, and OpenShift type: application -version: 1.0.1 -appVersion: "1.0.1" +version: 1.0.2 +appVersion: "1.0.2" keywords: - tracebloc - kubernetes diff --git a/client/templates/NOTES.txt b/client/templates/NOTES.txt index 4fa07fd..0230d78 100644 --- a/client/templates/NOTES.txt +++ b/client/templates/NOTES.txt @@ -18,7 +18,7 @@ {{ "\033[1;34m" }}Storage:{{ "\033[0m" }} {{ "\033[0;33m" }}hostPath (bare-metal){{ "\033[0m" }} {{ "\033[1;34m" }}Host dirs:{{ "\033[0m" }} {{ "\033[0;33m" }}/tracebloc/data, /tracebloc/logs, /tracebloc/mysql (on the node){{ "\033[0m" }} {{- else }} - {{ "\033[1;34m" }}Storage:{{ "\033[0m" }} {{ "\033[0;33m" }}dynamic PVC ({{ .Values.storageClass.name }}){{ "\033[0m" }} + {{ "\033[1;34m" }}Storage:{{ "\033[0m" }} {{ "\033[0;33m" }}dynamic PVC ({{ include "tracebloc.storageClassName" . }}){{ "\033[0m" }} {{- end }} {{- if .Values.openshift.scc.enabled }} {{ "\033[1;34m" }}OpenShift SCC:{{ "\033[0m" }} {{ "\033[0;33m" }}tracebloc-resource-monitor-{{ .Release.Name }}{{ "\033[0m" }} diff --git a/client/templates/_helpers.tpl b/client/templates/_helpers.tpl index 54062f8..abcb4a0 100644 --- a/client/templates/_helpers.tpl +++ b/client/templates/_helpers.tpl @@ -69,6 +69,26 @@ mysql-pvc {{ .Release.Name }}-regcred {{- end }} +{{/* + StorageClass name: when storageClass.create is true, use a release-unique name + so each release gets its own StorageClass (avoids Helm ownership conflicts). + When create is false, use the user-provided storageClass.name for an existing class. +*/}} +{{- define "tracebloc.storageClassName" -}} +{{- if .Values.storageClass.create -}} +{{ .Release.Name }}-storage-class +{{- else -}} +{{ .Values.storageClass.name }} +{{- end -}} +{{- end -}} + +{{/* Whether to create registry secret and add imagePullSecrets. Only when dockerRegistry is present and create is true; omit dockerRegistry or set create: false for public images. */}} +{{- define "tracebloc.useImagePullSecrets" -}} +{{- if and .Values.dockerRegistry (default false .Values.dockerRegistry.create) -}} +true +{{- end -}} +{{- end }} + {{/* Image reference — defaults to docker.io when no registry is provided. Tag defaults to "prod" when CLIENT_ENV is omitted or empty. diff --git a/client/templates/docker-registry-secret.yaml b/client/templates/docker-registry-secret.yaml index a47c40a..07ea427 100644 --- a/client/templates/docker-registry-secret.yaml +++ b/client/templates/docker-registry-secret.yaml @@ -1,3 +1,4 @@ +{{- if include "tracebloc.useImagePullSecrets" . }} apiVersion: v1 kind: Secret metadata: @@ -8,3 +9,4 @@ metadata: type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: {{ template "imagePullSecret" . }} +{{- end }} diff --git a/client/templates/jobs-manager-deployment.yaml b/client/templates/jobs-manager-deployment.yaml index 51aaec8..28c5df8 100644 --- a/client/templates/jobs-manager-deployment.yaml +++ b/client/templates/jobs-manager-deployment.yaml @@ -133,8 +133,10 @@ spec: value: {{ $value | quote }} {{- end }} {{- end }} + {{- if include "tracebloc.useImagePullSecrets" . }} imagePullSecrets: - name: {{ include "tracebloc.registrySecretName" . }} + {{- end }} volumes: - name: shared-volume persistentVolumeClaim: diff --git a/client/templates/logs-pvc.yaml b/client/templates/logs-pvc.yaml index 2ebcbc3..3f32c2a 100644 --- a/client/templates/logs-pvc.yaml +++ b/client/templates/logs-pvc.yaml @@ -8,7 +8,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} capacity: storage: {{ $storage }} accessModes: @@ -31,7 +31,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} accessModes: - {{ .Values.pvcAccessMode | default "ReadWriteMany" }} resources: diff --git a/client/templates/mysql-deployment.yaml b/client/templates/mysql-deployment.yaml index 0ae150a..0ed55f4 100644 --- a/client/templates/mysql-deployment.yaml +++ b/client/templates/mysql-deployment.yaml @@ -75,8 +75,10 @@ spec: mountPath: /etc/mysql/conf.d/ - name: mysql-logs mountPath: /var/log/mysql/ + {{- if include "tracebloc.useImagePullSecrets" . }} imagePullSecrets: - name: {{ include "tracebloc.registrySecretName" . }} + {{- end }} volumes: - name: mysql-persistent-storage persistentVolumeClaim: diff --git a/client/templates/mysql-storage-pvc.yaml b/client/templates/mysql-storage-pvc.yaml index f63ad33..fcb77af 100644 --- a/client/templates/mysql-storage-pvc.yaml +++ b/client/templates/mysql-storage-pvc.yaml @@ -8,7 +8,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} capacity: storage: {{ $storage }} accessModes: @@ -31,7 +31,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} accessModes: - {{ .Values.pvcAccessMode | default "ReadWriteMany" }} resources: diff --git a/client/templates/resource-monitor-daemonset.yaml b/client/templates/resource-monitor-daemonset.yaml index 003b14a..31b6696 100644 --- a/client/templates/resource-monitor-daemonset.yaml +++ b/client/templates/resource-monitor-daemonset.yaml @@ -76,8 +76,10 @@ spec: cpu: 200m memory: 256Mi terminationGracePeriodSeconds: 15 + {{- if include "tracebloc.useImagePullSecrets" . }} imagePullSecrets: - name: {{ include "tracebloc.registrySecretName" . }} + {{- end }} volumes: - name: host-proc hostPath: diff --git a/client/templates/shared-images-pvc.yaml b/client/templates/shared-images-pvc.yaml index cfa88f8..aa4138a 100644 --- a/client/templates/shared-images-pvc.yaml +++ b/client/templates/shared-images-pvc.yaml @@ -8,7 +8,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} capacity: storage: {{ $storage }} accessModes: @@ -31,7 +31,7 @@ metadata: labels: {{- include "tracebloc.labels" . | nindent 4 }} spec: - storageClassName: {{ .Values.storageClass.name }} + storageClassName: {{ include "tracebloc.storageClassName" . }} accessModes: - {{ .Values.pvcAccessMode | default "ReadWriteMany" }} resources: diff --git a/client/templates/storage-class.yaml b/client/templates/storage-class.yaml index e49f656..4124fce 100644 --- a/client/templates/storage-class.yaml +++ b/client/templates/storage-class.yaml @@ -2,7 +2,7 @@ apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: - name: {{ .Values.storageClass.name }} + name: {{ include "tracebloc.storageClassName" . }} labels: {{- include "tracebloc.labels" . | nindent 4 }} provisioner: {{ required "storageClass.provisioner is required when storageClass.create is true" .Values.storageClass.provisioner }} diff --git a/client/tests/secrets_test.yaml b/client/tests/secrets_test.yaml index 463a002..3d027cc 100644 --- a/client/tests/secrets_test.yaml +++ b/client/tests/secrets_test.yaml @@ -25,10 +25,11 @@ tests: path: metadata.labels["app.kubernetes.io/managed-by"] pattern: Helm - - it: should create docker registry secret + - it: should create docker registry secret when create is true template: templates/docker-registry-secret.yaml set: dockerRegistry: + create: true server: https://index.docker.io/v1/ username: testuser password: testpass @@ -44,3 +45,24 @@ tests: value: kubernetes.io/dockerconfigjson - isNotEmpty: path: data[".dockerconfigjson"] + + - it: should not create docker registry secret when create is omitted (default false) + template: templates/docker-registry-secret.yaml + set: + dockerRegistry: + server: https://index.docker.io/v1/ + username: testuser + password: testpass + email: test@test.com + asserts: + - hasDocuments: + count: 0 + + - it: should not create docker registry secret when dockerRegistry is omitted (public images) + template: templates/docker-registry-secret.yaml + values: + - values-public-images.yaml + asserts: + - hasDocuments: + count: 0 + diff --git a/client/tests/values-public-images.yaml b/client/tests/values-public-images.yaml new file mode 100644 index 0000000..9e5359c --- /dev/null +++ b/client/tests/values-public-images.yaml @@ -0,0 +1,3 @@ +# Use this values file to test public images (no registry secret). +# When dockerRegistry is null, no registry secret is created and no imagePullSecrets are added. +dockerRegistry: null diff --git a/client/values.schema.json b/client/values.schema.json index 40996aa..d244ac8 100644 --- a/client/values.schema.json +++ b/client/values.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft-07/schema#", "title": "Tracebloc Helm Chart Values", "type": "object", - "required": ["clientId", "clientPassword", "dockerRegistry"], + "required": ["clientId", "clientPassword"], "properties": { "env": { "type": "object", @@ -156,9 +156,14 @@ "description": "Client authentication password" }, "dockerRegistry": { - "type": "object", - "required": ["server", "username", "password", "email"], + "type": ["object", "null"], + "description": "Optional. Omit entirely or set null for public images (no secret or imagePullSecrets). Only create when set and create is true.", "properties": { + "create": { + "type": "boolean", + "default": false, + "description": "When true, create registry secret and add imagePullSecrets to workloads. Omit dockerRegistry or set false for public images." + }, "server": { "type": "string", "format": "uri" @@ -172,7 +177,20 @@ "email": { "type": "string" } - } + }, + "allOf": [ + { + "if": { + "properties": { + "create": { "const": true } + }, + "required": ["create"] + }, + "then": { + "required": ["server", "username", "password", "email"] + } + } + ] } } } diff --git a/client/values.yaml b/client/values.yaml index c3deb7e..5f69796 100644 --- a/client/values.yaml +++ b/client/values.yaml @@ -25,10 +25,12 @@ env: { # RUNTIME_CLASS_NAME: "" } # -- StorageClass configuration -# Set create: false to use an existing storage class +# When create: true, the StorageClass name is release-unique (e.g. "-storage-class") +# so each release gets its own class and Helm ownership conflicts are avoided. +# Set create: false to use an existing storage class; then name must match that class. storageClass: create: true - name: client-storage-class + name: client-storage-class # only used when create: false provisioner: "" allowVolumeExpansion: true # Optional fields — omitted from rendered YAML when empty @@ -71,10 +73,12 @@ openshift: clientId: "" clientPassword: "" -# -- Docker registry credentials -# Secret name is auto-generated as {{ .Release.Name }}-regcred -dockerRegistry: - server: https://index.docker.io/v1/ - username: "" - password: "" - email: "" +# -- Docker registry credentials (optional; only used when dockerRegistry is set and create is true) +# Omit dockerRegistry entirely, or set create: false, for public images (no imagePullSecrets). +# When create is true, secret name is {{ .Release.Name }}-regcred. +# dockerRegistry: +# create: true +# server: https://index.docker.io/v1/ +# username: "" +# password: "" +# email: "" diff --git a/scripts/install-k8s.ps1 b/scripts/install-k8s.ps1 index abd01bb..711ddbb 100644 --- a/scripts/install-k8s.ps1 +++ b/scripts/install-k8s.ps1 @@ -20,6 +20,7 @@ # $env:HTTP_PORT = "80" default: 80 # $env:HTTPS_PORT = "443" default: 443 # $env:HOST_DATA_DIR = "C:\data" default: $env:USERPROFILE\.tracebloc +# $env:CLIENT_ENV = "dev" optional; if not set, CLIENT_ENV is not added to env in values # ============================================================================= #Requires -Version 5.1 @@ -103,6 +104,7 @@ $K8S_VERSION = if ($env:K8S_VERSION) { $env:K8S_VERSION } else { "v1.29.4- $HTTP_PORT = if ($env:HTTP_PORT) { $env:HTTP_PORT } else { "80" } $HTTPS_PORT = if ($env:HTTPS_PORT) { $env:HTTPS_PORT } else { "443" } $HOST_DATA_DIR = if ($env:HOST_DATA_DIR) { $env:HOST_DATA_DIR } else { "$env:USERPROFILE\.tracebloc" } +$CLIENT_ENV = $env:CLIENT_ENV # if not set, do not add CLIENT_ENV to env in values $GPU_VENDOR = "none" # nvidia | amd | amd_unsupported | none $NVIDIA_DRIVER_OK = $false @@ -128,6 +130,7 @@ Environment variable overrides: HTTP_PORT Host HTTP ingress port (default: 80) HTTPS_PORT Host HTTPS ingress port (default: 443) HOST_DATA_DIR Persistent data directory (default: ~\.tracebloc) + CLIENT_ENV Client env (e.g. prod, dev); if not set, not added to values macOS / Linux: curl -fsSL https://raw.githubusercontent.com/tracebloc/client/main/scripts/install.sh | bash @@ -503,7 +506,7 @@ function Install-Kubectl { # ============================================================================= function Install-K3dAndHelm { - Step "Step 5/5 -- k3d and Helm" + Step "Step 5/6 -- k3d and Helm" # -- k3d -- if (-not (Has "k3d")) { @@ -675,6 +678,175 @@ function Confirm-GpuNode { else { Warn "GPU not yet visible. Re-check: kubectl describe node | Select-String 'nvidia'" } } +# ============================================================================= +# STEP 6 -- INSTALL TRACEBLOC CLIENT HELM CHART +# ============================================================================= + +$TRACEBLOC_HELM_REPO_URL = "https://tracebloc.github.io/client" +$TRACEBLOC_HELM_REPO_NAME = "tracebloc" +$TRACEBLOC_CHART_NAME = "client" + + +function Get-TraceblocYamlValue { + param([string]$Path, [string]$Key) + if (-not (Test-Path $Path)) { return "" } + $line = Get-Content $Path -ErrorAction SilentlyContinue | Where-Object { $_ -match "^\s*${Key}\s*:" } | Select-Object -First 1 + if (-not $line) { return "" } + $val = $line -replace "^\s*${Key}\s*:\s*", "" + $val = $val.Trim() + + # Handle quoted YAML scalars and unescape single-quoted style + if ($val.StartsWith("'") -and $val.EndsWith("'") -and $val.Length -ge 2) { + # Strip surrounding single quotes + $val = $val.Substring(1, $val.Length - 2) + # YAML single-quoted style uses '' to represent a literal ' + $val = $val -replace "''", "'" + } elseif ($val.StartsWith('"') -and $val.EndsWith('"') -and $val.Length -ge 2) { + # Strip surrounding double quotes + $val = $val.Substring(1, $val.Length - 2) + } + + return $val +} + +function Install-ClientHelm { + Step "Step 6/6 -- Installing Tracebloc client Helm chart" + + if (-not (Test-Path $HOST_DATA_DIR)) { + New-Item -ItemType Directory -Path $HOST_DATA_DIR -Force | Out-Null + } + $valuesFile = Join-Path $HOST_DATA_DIR "values.yaml" + + $defaultNamespace = "default" + $defaultClientId = "" + $defaultClientPassword = "" + + if (Test-Path $valuesFile) { + Info "Existing values file found: $valuesFile" + do { + $useExisting = Read-Host "Use values from it as defaults? [Y/n]" + $useExisting = if ($useExisting) { $useExisting.Trim().ToLowerInvariant() } else { "y" } + if ($useExisting -eq "y" -or $useExisting -eq "yes" -or $useExisting -eq "n" -or $useExisting -eq "no" -or $useExisting -eq "") { break } + Warn "Please enter y or n." + } while ($true) + + if ($useExisting -eq "y" -or $useExisting -eq "yes" -or $useExisting -eq "") { + $defaultClientId = Get-TraceblocYamlValue -Path $valuesFile -Key "clientId" + $defaultClientPassword = Get-TraceblocYamlValue -Path $valuesFile -Key "clientPassword" + if ($defaultClientId) { Info "Using existing clientId as default." } + if ($defaultClientPassword) { Info "Using existing clientPassword as default." } + } + } + + Info "Enter values for the Tracebloc client installation:" + $namespacePrompt = if ($defaultNamespace) { "Namespace [$defaultNamespace]" } else { "Namespace [default]" } + $nsInput = Read-Host $namespacePrompt + $TB_NAMESPACE = if ($nsInput) { $nsInput } else { $defaultNamespace } + + Write-Host "" + Step "Client ID & Password" + Write-Host "Need credentials? Create a client at: " -NoNewline; Write-Host "https://ai.tracebloc.io/clients" -ForegroundColor White + Write-Host "Setting up a client is free." -ForegroundColor Yellow + Write-Host "" + if ($defaultClientId) { + $idInput = Read-Host "Client ID [$defaultClientId]" + $TB_CLIENT_ID = if ($idInput) { $idInput } else { $defaultClientId } + } else { + $TB_CLIENT_ID = Read-Host "Client ID" + } + if (-not $TB_CLIENT_ID) { Err "Client ID cannot be empty." } + + if ($defaultClientPassword) { + $pwInput = Read-Host "Client password [press Enter to keep existing]" -AsSecureString + if ($pwInput -and $pwInput.Length -gt 0) { + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwInput) + try { $TB_CLIENT_PASSWORD = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } + } else { + $TB_CLIENT_PASSWORD = $defaultClientPassword + } + } else { + $pwInput = Read-Host "Client password" -AsSecureString + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwInput) + try { $TB_CLIENT_PASSWORD = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } + } + if (-not $TB_CLIENT_PASSWORD) { Err "Client password cannot be empty." } + + $passwordEscaped = $TB_CLIENT_PASSWORD -replace "'", "''" + + $gpuVal = "" + if ($GPU_VENDOR -eq "nvidia" -and $NVIDIA_DRIVER_OK) { + $gpuVal = "nvidia.com/gpu=1" + Info "NVIDIA GPU detected -- setting GPU_LIMITS and GPU_REQUESTS to nvidia.com/gpu=1" + } else { + Info "No NVIDIA GPU -- GPU_LIMITS and GPU_REQUESTS left empty" + } + + Info "Writing values to $valuesFile" + $envBlock = "env:`n" + if ($CLIENT_ENV) { + $envBlock += " CLIENT_ENV: $CLIENT_ENV`n" + } + $envBlock += @" + RESOURCE_LIMITS: "cpu=2,memory=8Gi" + RESOURCE_REQUESTS: "cpu=2,memory=8Gi" + GPU_LIMITS: "$gpuVal" + GPU_REQUESTS: "$gpuVal" + RUNTIME_CLASS_NAME: "" + +storageClass: + create: true + name: client-storage-class + provisioner: manual + allowVolumeExpansion: true + parameters: {} + +hostPath: + enabled: true + +pvc: + mysql: 2Gi + logs: 10Gi + data: 50Gi + +pvcAccessMode: ReadWriteOnce + +clusterScope: true + +clientId: "$TB_CLIENT_ID" +clientPassword: '$passwordEscaped' + +"@ + $valuesContent = @" +# ============================================================ +# Generated by install-k8s.ps1 -- Tracebloc client Helm values +# ============================================================ + +$envBlock +"@ + Set-Content -Path $valuesFile -Value $valuesContent -Encoding UTF8 + Ok "Values file written to $valuesFile" + + $repoList = helm repo list 2>&1 | Out-String + if ($repoList -notmatch [regex]::Escape($TRACEBLOC_HELM_REPO_NAME)) { + Info "Adding Helm repo: $TRACEBLOC_HELM_REPO_URL" + helm repo add $TRACEBLOC_HELM_REPO_NAME $TRACEBLOC_HELM_REPO_URL + if ($LASTEXITCODE -ne 0) { Err "Failed to add Helm repo." } + } + Info "Updating Helm repos..." + helm repo update + if ($LASTEXITCODE -ne 0) { Warn "helm repo update had issues -- continuing." } + + Info "Installing $TB_NAMESPACE from $TRACEBLOC_HELM_REPO_NAME/$TRACEBLOC_CHART_NAME in namespace '$TB_NAMESPACE'..." + helm upgrade --install $TB_NAMESPACE "$TRACEBLOC_HELM_REPO_NAME/$TRACEBLOC_CHART_NAME" ` + --namespace $TB_NAMESPACE ` + --create-namespace ` + --values $valuesFile + if ($LASTEXITCODE -ne 0) { Err "Helm install failed -- see output above." } + + Ok "Tracebloc client Helm chart installed in namespace '$TB_NAMESPACE'." + Info "Values file: $valuesFile" +} + # ============================================================================= # CLUSTER VERIFICATION # ============================================================================= @@ -754,6 +926,7 @@ Install-K3dAndHelm New-K3dCluster Install-GpuDevicePlugin Confirm-GpuNode +Install-ClientHelm Confirm-Cluster Print-Summary diff --git a/scripts/install-k8s.sh b/scripts/install-k8s.sh index 013af32..47810ca 100755 --- a/scripts/install-k8s.sh +++ b/scripts/install-k8s.sh @@ -21,6 +21,7 @@ # HTTP_PORT=80 default: 80 (host → cluster ingress) # HTTPS_PORT=443 default: 443 # HOST_DATA_DIR=~/.tracebloc default: ~/.tracebloc +# CLIENT_ENV=dev optional; if not set, CLIENT_ENV is not added to env in values # TRACEBLOC_SKIP_REBOOT_PROMPT=1 (Linux) skip "Reboot now?" after NVIDIA driver install # ============================================================================= @@ -48,6 +49,7 @@ source "${LIB_DIR}/setup-macos.sh" source "${LIB_DIR}/setup-linux.sh" source "${LIB_DIR}/cluster.sh" source "${LIB_DIR}/gpu-plugins.sh" +source "${LIB_DIR}/install-client-helm.sh" source "${LIB_DIR}/summary.sh" trap install_cleanup EXIT @@ -73,6 +75,7 @@ main() { create_cluster deploy_gpu_device_plugin verify_gpu + install_client_helm verify_cluster print_summary } diff --git a/scripts/install.sh b/scripts/install.sh index 3e58032..f7e8f6a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -5,6 +5,7 @@ # Usage (macOS / Linux): # curl -fsSL https://raw.githubusercontent.com/tracebloc/client/main/scripts/install.sh | bash # curl -fsSL ... | BRANCH=develop bash +# curl -fsSL ... | BRANCH=develop CLIENT_ENV=dev bash # # Windows (PowerShell as Administrator): # irm https://raw.githubusercontent.com/tracebloc/client/main/scripts/install.ps1 | iex @@ -39,6 +40,7 @@ FILES=( "scripts/lib/setup-linux.sh" "scripts/lib/cluster.sh" "scripts/lib/gpu-plugins.sh" + "scripts/lib/install-client-helm.sh" "scripts/lib/summary.sh" ) diff --git a/scripts/lib/cluster.sh b/scripts/lib/cluster.sh index 64f62e9..b16b0d2 100755 --- a/scripts/lib/cluster.sh +++ b/scripts/lib/cluster.sh @@ -12,9 +12,18 @@ _cluster_exists() { fi } +# Ensure host dirs exist so /tracebloc/data, /tracebloc/logs, /tracebloc/mysql exist inside nodes (HOST_DATA_DIR is mounted as /tracebloc). +# Only chmod the container data subdirs; do not make HOST_DATA_DIR or files like values.yaml world-readable. +_ensure_tracebloc_dirs() { + mkdir -p "$HOST_DATA_DIR" "$HOST_DATA_DIR/data" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/mysql" + chmod -R 777 "$HOST_DATA_DIR/data" "$HOST_DATA_DIR/logs" "$HOST_DATA_DIR/mysql" 2>/dev/null || true +} + create_cluster() { step "Creating k3d Cluster: '$CLUSTER_NAME'" + _ensure_tracebloc_dirs + if _cluster_exists; then _handle_existing_cluster else @@ -49,11 +58,7 @@ _handle_existing_cluster() { } _create_new_cluster() { - if [[ ! -d "$HOST_DATA_DIR" ]]; then - info "Creating host data directory: $HOST_DATA_DIR" - mkdir -p "$HOST_DATA_DIR" - fi - + # HOST_DATA_DIR and data/logs/mysql subdirs already created by _ensure_tracebloc_dirs K3D_ARGS=( cluster create "$CLUSTER_NAME" --servers "$SERVERS" diff --git a/scripts/lib/install-client-helm.sh b/scripts/lib/install-client-helm.sh new file mode 100644 index 0000000..97c3450 --- /dev/null +++ b/scripts/lib/install-client-helm.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# ============================================================================= +# install-client-helm.sh — Install Tracebloc client Helm chart (step 6) +# Generates values from defaults + user prompts (namespace, clientId, clientPassword) +# and GPU detection. Values file is written to HOST_DATA_DIR/values.yaml. +# ============================================================================= + +TRACEBLOC_HELM_REPO_URL="https://tracebloc.github.io/client" +TRACEBLOC_HELM_REPO_NAME="tracebloc" +TRACEBLOC_CHART_NAME="client" + +# Extract a key's value from a simple YAML file (handles "value", 'value', or value) +# For single-quoted YAML values, unescapes '' back to ' so credentials round-trip correctly. +_extract_yaml_value() { + local file="$1" key="$2" + local line + line=$(grep -E "^${key}:" "$file" 2>/dev/null | head -1) + [[ -z "$line" ]] && return + line="${line#*:}" + line="${line#"${line%%[![:space:]]*}"}" + if [[ "$line" == \'*\' ]]; then + line="${line#\'}" + line="${line%\'}" + line="${line//\'\'/\'}" + else + line="${line#\"}" + line="${line%\"}" + fi + printf '%s' "$line" +} + +install_client_helm() { + step "Installing Tracebloc client Helm chart" + + _ensure_tracebloc_dirs + local values_file="${HOST_DATA_DIR}/values.yaml" + + local use_existing="" + local default_namespace="default" + local default_client_id="" + local default_client_password="" + + if [[ -f "$values_file" ]]; then + info "Existing values file found: $values_file" + while true; do + read -r -p "Use values from it as defaults? [Y/n]: " use_existing + use_existing="$(echo "${use_existing}" | tr '[:upper:]' '[:lower:]')" + [[ "$use_existing" == "y" || "$use_existing" == "yes" || "$use_existing" == "n" || "$use_existing" == "no" || -z "$use_existing" ]] && break + warn "Please enter y or n." + done + if [[ "$use_existing" == "y" || "$use_existing" == "yes" || -z "$use_existing" ]]; then + default_client_id=$(_extract_yaml_value "$values_file" "clientId") + default_client_password=$(_extract_yaml_value "$values_file" "clientPassword") + [[ -n "$default_client_id" ]] && info "Using existing clientId as default." + [[ -n "$default_client_password" ]] && info "Using existing clientPassword as default." + fi + fi + + # ── Prompt for user inputs ───────────────────────────────────────────────── + info "Enter values for the Tracebloc client installation:" + read -r -p "Namespace [${default_namespace}]: " TB_NAMESPACE_INPUT + TB_NAMESPACE="${TB_NAMESPACE_INPUT:-$default_namespace}" + + echo "" + step "Client ID & Password" + echo -e "${BOLD}${YELLOW}Need credentials? Create a client at: ${RESET}${BOLD}\033[1;37mhttps://ai.tracebloc.io/clients${RESET}" + echo -e "${BOLD}${YELLOW}Setting up a client is free.${RESET}" + echo "" + if [[ -n "$default_client_id" ]]; then + read -r -p "Client ID [${default_client_id}]: " TB_CLIENT_ID_INPUT + TB_CLIENT_ID="${TB_CLIENT_ID_INPUT:-$default_client_id}" + else + read -r -p "Client ID: " TB_CLIENT_ID + fi + [[ -z "$TB_CLIENT_ID" ]] && error "Client ID cannot be empty." + + if [[ -n "$default_client_password" ]]; then + read -r -s -p "Client password [press Enter to keep existing]: " TB_CLIENT_PASSWORD_INPUT + echo "" + TB_CLIENT_PASSWORD="${TB_CLIENT_PASSWORD_INPUT:-$default_client_password}" + else + read -r -s -p "Client password: " TB_CLIENT_PASSWORD + echo "" + fi + [[ -z "$TB_CLIENT_PASSWORD" ]] && error "Client password cannot be empty." + + # Escape single quotes for YAML: ' -> '' + TB_CLIENT_PASSWORD_ESCAPED="${TB_CLIENT_PASSWORD//\'/\'\'}" + + # ── GPU limits: nvidia.com/gpu=1 if NVIDIA GPU available, else "" ────────── + local gpu_val + if [[ "${GPU_VENDOR:-}" == "nvidia" ]]; then + gpu_val="nvidia.com/gpu=1" + info "NVIDIA GPU detected — setting GPU_LIMITS and GPU_REQUESTS to nvidia.com/gpu=1" + else + gpu_val="" + info "No NVIDIA GPU — GPU_LIMITS and GPU_REQUESTS left empty" + fi + + # ── Write generated values.yaml to .tracebloc ────────────────────────────── + info "Writing values to $values_file" + cat < "$values_file" +# ============================================================ +# Generated by install-k8s.sh — Tracebloc client Helm values +# ============================================================ + +env: +$([ -n "${CLIENT_ENV:-}" ] && printf ' CLIENT_ENV: "%s"\n' "$CLIENT_ENV") + RESOURCE_LIMITS: "cpu=2,memory=8Gi" + RESOURCE_REQUESTS: "cpu=2,memory=8Gi" + GPU_LIMITS: "$gpu_val" + GPU_REQUESTS: "$gpu_val" + RUNTIME_CLASS_NAME: "" + +storageClass: + create: true + name: client-storage-class + provisioner: manual + allowVolumeExpansion: true + parameters: {} + +hostPath: + enabled: true + +pvc: + mysql: 2Gi + logs: 10Gi + data: 50Gi + +pvcAccessMode: ReadWriteOnce + +clusterScope: true + +clientId: "$TB_CLIENT_ID" +clientPassword: '$TB_CLIENT_PASSWORD_ESCAPED' + +EOF + + chmod 600 "$values_file" 2>/dev/null || true + success "Values file written to $values_file" + + # ── Add repo and install ─────────────────────────────────────────────────── + if ! helm repo list 2>/dev/null | grep -q "^${TRACEBLOC_HELM_REPO_NAME}[[:space:]]"; then + info "Adding Helm repo: $TRACEBLOC_HELM_REPO_URL" + helm repo add "$TRACEBLOC_HELM_REPO_NAME" "$TRACEBLOC_HELM_REPO_URL" + fi + info "Updating Helm repos..." + helm repo update + + info "Installing $TB_NAMESPACE from $TRACEBLOC_HELM_REPO_NAME/$TRACEBLOC_CHART_NAME in namespace '$TB_NAMESPACE'..." + helm upgrade --install "$TB_NAMESPACE" "$TRACEBLOC_HELM_REPO_NAME/$TRACEBLOC_CHART_NAME" \ + --namespace "$TB_NAMESPACE" \ + --create-namespace \ + --values "$values_file" + + success "Tracebloc client Helm chart installed in namespace '$TB_NAMESPACE'." + info "Values file: $values_file" +}