Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1239 lines
58 KiB
Plaintext
1239 lines
58 KiB
Plaintext
use "config.nu" *
|
|
|
|
# Helper function to sanitize branch names for Kubernetes resources
|
|
def sanitize-branch-name [branch: string] {
|
|
# Sanitize branch name for Kubernetes namespace (lowercase, alphanumeric and hyphens only)
|
|
# First replace slashes with hyphens, then clean up any other special characters
|
|
let sanitized_base = ($branch | str replace -a "/" "-" | str downcase | str replace -a "[^a-z0-9-]" "-" | str replace -r "^-+|-+$" "" | str replace -r "-+" "-")
|
|
|
|
# Ensure Helm release name stays under 53 character limit
|
|
# Release name format: "alga-dev-{sanitized_branch}" = 9 + sanitized_branch length
|
|
let max_branch_length = 43 # 53 - 9 = 44, but we need 43 to be safe
|
|
if ($sanitized_base | str length) > $max_branch_length {
|
|
# For long names, take first part and add hash of full name for uniqueness
|
|
let hash_suffix = ($sanitized_base | hash sha256 | str substring 0..7)
|
|
let prefix_length = $max_branch_length - 9 # 9 chars for "-" + 8-char hash
|
|
let prefix = ($sanitized_base | str substring 0..$prefix_length)
|
|
$"($prefix)-($hash_suffix)"
|
|
} else {
|
|
$sanitized_base
|
|
}
|
|
}
|
|
|
|
# Start development environment with Docker Compose
|
|
export def dev-up [
|
|
--detached (-d) # Run in detached mode (background)
|
|
--edition (-e): string = "ce" # Edition to start: ce (community) or ee (enterprise)
|
|
] {
|
|
let project_root = find-project-root
|
|
|
|
# Validate edition parameter
|
|
if not ($edition in ["ce", "ee"]) {
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Invalid edition '($edition)'. Must be 'ce' (community) or 'ee' (enterprise).($env.ALGA_COLOR_RESET)" }
|
|
}
|
|
|
|
let edition_file = if $edition == "ce" { "docker-compose.prebuilt.ce.yaml" } else { "docker-compose.ee.yaml" }
|
|
let base_file = if $edition == "ce" { "docker-compose.prebuilt.base.yaml" } else { "docker-compose.base.yaml" }
|
|
let edition_name = if $edition == "ce" { "Community Edition" } else { "Enterprise Edition" }
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Starting development environment (($edition_name))...($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Project root: ($project_root)($env.ALGA_COLOR_RESET)"
|
|
|
|
if $detached {
|
|
let command = $"docker compose -f ($base_file) -f ($edition_file) --env-file server/.env up --build -d"
|
|
print $"($env.ALGA_COLOR_YELLOW)Running: ($command)($env.ALGA_COLOR_RESET)"
|
|
|
|
let result = do {
|
|
cd $project_root
|
|
docker compose -f $base_file -f $edition_file --env-file server/.env up -d | complete
|
|
}
|
|
|
|
if $result.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Development environment (($edition_name)) started in background.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Access the application at: http://localhost:3000($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)View logs with: docker compose logs -f($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $"($env.ALGA_COLOR_RED)($result.stderr)($env.ALGA_COLOR_RESET)"
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Failed to start development environment($env.ALGA_COLOR_RESET)", code: $result.exit_code }
|
|
}
|
|
} else {
|
|
let command = $"docker compose -f ($base_file) -f ($edition_file) --env-file server/.env up"
|
|
print $"($env.ALGA_COLOR_YELLOW)Running: ($command)($env.ALGA_COLOR_RESET)"
|
|
|
|
# Stream output directly without capturing
|
|
cd $project_root
|
|
docker compose -f $base_file -f $edition_file --env-file server/.env up --build
|
|
}
|
|
}
|
|
|
|
# Stop development environment
|
|
export def dev-down [] {
|
|
let project_root = find-project-root
|
|
print $"($env.ALGA_COLOR_CYAN)Stopping development environment...($env.ALGA_COLOR_RESET)"
|
|
|
|
let result = do {
|
|
cd $project_root
|
|
docker compose down | complete
|
|
}
|
|
|
|
if $result.exit_code == 0 {
|
|
print $result.stdout
|
|
print $"($env.ALGA_COLOR_GREEN)Development environment stopped.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $"($env.ALGA_COLOR_RED)($result.stderr)($env.ALGA_COLOR_RESET)"
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Failed to stop development environment($env.ALGA_COLOR_RESET)", code: $result.exit_code }
|
|
}
|
|
}
|
|
|
|
# Create development environment for branch
|
|
#
|
|
# Environment variables for AI automation (read from .env file or shell environment):
|
|
# CUSTOM_OPENAI_API_KEY: Required - API key for LLM provider (e.g., OpenRouter key)
|
|
# CUSTOM_OPENAI_BASE_URL: Optional - API endpoint (default: https://openrouter.ai/api/v1)
|
|
# CUSTOM_OPENAI_MODEL: Optional - Model name (default: google/gemini-2.5-flash-preview-05-20)
|
|
#
|
|
export def dev-env-create [
|
|
branch: string # Git branch name
|
|
--edition: string = "ee" # Edition: ce or ee
|
|
--use-latest = false # Use 'latest' tag instead of unique tag
|
|
--checkout = true # Checkout the branch locally
|
|
--from-tag: string = "latest" # Deploy from existing image tag instead of building
|
|
--author-name: string = "" # Git author name (e.g., "John Doe")
|
|
--author-email: string = "" # Git author email (e.g., "john@example.com")
|
|
] {
|
|
let project_root = find-project-root
|
|
|
|
# Validate edition parameter
|
|
if not ($edition in ["ce", "ee"]) {
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Invalid edition '($edition)'. Must be 'ce' (community) or 'ee' (enterprise).($env.ALGA_COLOR_RESET)" }
|
|
}
|
|
|
|
# Check for mutually exclusive options
|
|
if ($from_tag | str length) > 0 and $use_latest {
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Cannot use both --from-tag and --use-latest. Choose one or the other.($env.ALGA_COLOR_RESET)" }
|
|
}
|
|
|
|
# Sanitize branch name for Kubernetes namespace and Helm release name length limits
|
|
let sanitized_branch = (sanitize-branch-name $branch)
|
|
|
|
let namespace = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Checkout the branch if requested
|
|
if $checkout {
|
|
print $"($env.ALGA_COLOR_CYAN)Checking out branch: ($branch)($env.ALGA_COLOR_RESET)"
|
|
let checkout_result = do {
|
|
cd $project_root
|
|
git checkout $branch | complete
|
|
}
|
|
|
|
if $checkout_result.exit_code != 0 {
|
|
# Try to fetch and checkout if branch doesn't exist locally
|
|
print $"($env.ALGA_COLOR_YELLOW)Branch not found locally, fetching from remote...($env.ALGA_COLOR_RESET)"
|
|
let fetch_result = do {
|
|
cd $project_root
|
|
git fetch origin $branch | complete
|
|
}
|
|
|
|
if $fetch_result.exit_code == 0 {
|
|
let checkout_retry = do {
|
|
cd $project_root
|
|
git checkout -b $branch origin/$branch | complete
|
|
}
|
|
|
|
if $checkout_retry.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Could not checkout branch ($branch). Continuing with current branch.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
} else {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Branch ($branch) not found in remote. Continuing with current branch.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Find available ports for external access
|
|
print $"($env.ALGA_COLOR_CYAN)Finding available ports for external access...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Function to find a free port
|
|
def find-free-port [start_port: int] {
|
|
mut port = $start_port
|
|
mut found = false
|
|
|
|
while not $found and $port < 65535 {
|
|
# Copy mutable variable to avoid capture issue
|
|
let current_port = $port
|
|
|
|
# Check if port is in use
|
|
let check_result = do {
|
|
bash -c $"nc -z localhost ($current_port) 2>/dev/null" | complete
|
|
}
|
|
|
|
if $check_result.exit_code != 0 {
|
|
# Port is free
|
|
$found = true
|
|
} else {
|
|
$port = $port + 1
|
|
}
|
|
}
|
|
|
|
if $found { $port } else { 0 }
|
|
}
|
|
|
|
# Find ports for each service
|
|
let app_port = find-free-port 30000
|
|
let code_server_port = find-free-port ($app_port + 1)
|
|
let code_app_port = find-free-port ($code_server_port + 1)
|
|
let ai_web_port = find-free-port ($code_app_port + 1)
|
|
|
|
if $app_port == 0 or $code_server_port == 0 or $code_app_port == 0 or $ai_web_port == 0 {
|
|
error make { msg: $"($env.ALGA_COLOR_RED)Could not find available ports for services($env.ALGA_COLOR_RESET)" }
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_GREEN)Assigned external ports:($env.ALGA_COLOR_RESET)"
|
|
print $" Main App: ($app_port)"
|
|
print $" Code Server: ($code_server_port)"
|
|
print $" Code App: ($code_app_port)"
|
|
print $" AI Web: ($ai_web_port)"
|
|
|
|
# Determine image tag and whether to build
|
|
let tag_info = if ($from_tag | str length) > 0 {
|
|
# User explicitly provided a tag, so don't build
|
|
{ tag: $from_tag, build: false, reason: $"using provided tag '($from_tag)'" }
|
|
} else if $use_latest {
|
|
# User wants to build and tag as 'latest'
|
|
{ tag: "latest", build: true, reason: "building and using 'latest' tag" }
|
|
} else {
|
|
# Default behavior: use existing 'latest' tag without building
|
|
{ tag: "latest", build: false, reason: "using default 'latest' tag" }
|
|
}
|
|
|
|
let image_tag = $tag_info.tag
|
|
let should_build = $tag_info.build
|
|
let reason = $tag_info.reason
|
|
|
|
# Build image only if determined necessary
|
|
if $should_build {
|
|
print $"($env.ALGA_COLOR_CYAN)Building image before deployment... ($reason)($env.ALGA_COLOR_RESET)"
|
|
# The --use-latest flag in build-image handles tagging with both SHA and 'latest'
|
|
if $image_tag == "latest" {
|
|
build-image $edition --use-latest --push
|
|
} else {
|
|
build-image $edition --tag $image_tag --push
|
|
}
|
|
} else {
|
|
print $"($env.ALGA_COLOR_CYAN)Skipping build - ($reason)($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Creating development environment for branch: ($branch)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Sanitized name: ($sanitized_branch)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Edition: ($edition)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)AI Automation: enabled($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Namespace: ($namespace)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Image Tag: ($image_tag)($env.ALGA_COLOR_RESET)"
|
|
|
|
# Check if environment already exists
|
|
let existing_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $existing_check.exit_code == 0 {
|
|
# Check if namespace is in Terminating state
|
|
let namespace_status = do {
|
|
kubectl get namespace $namespace -o jsonpath='{.status.phase}' | complete
|
|
}
|
|
|
|
if $namespace_status.exit_code == 0 and ($namespace_status.stdout | str trim) == "Terminating" {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Namespace ($namespace) is stuck in Terminating state.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Attempting to force cleanup...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Force cleanup the stuck namespace
|
|
let force_cleanup = do {
|
|
kubectl delete namespace $namespace --grace-period=0 --force | complete
|
|
}
|
|
|
|
if $force_cleanup.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Stuck namespace cleaned up. Proceeding with creation...($env.ALGA_COLOR_RESET)"
|
|
sleep 5sec # Give it a moment to fully clear
|
|
} else {
|
|
print $"($env.ALGA_COLOR_RED)Failed to cleanup stuck namespace. Manual intervention required.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Try running: kubectl delete namespace ($namespace) --grace-period=0 --force($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
} else {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Environment for branch ($branch) already exists in namespace ($namespace)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-destroy ($branch)' to remove it first if you want to recreate it.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
}
|
|
|
|
# Create temporary values file (replace slashes with dashes in filename)
|
|
let safe_filename = ($branch | str replace -a "/" "-")
|
|
let temp_values_file = $"($project_root)/temp-values-($safe_filename).yaml"
|
|
let edition_comment = if $edition == "ee" { "# Enterprise Edition settings" } else { "# Community Edition settings" }
|
|
let image_name = $"harbor.nineminds.com/nineminds/alga-psa-($edition)"
|
|
|
|
# Load environment variables from user's home .env file if it exists
|
|
let home_env_file = ($nu.home-path | path join ".env")
|
|
if ($home_env_file | path exists) {
|
|
print $"($env.ALGA_COLOR_CYAN)Loading environment variables from ($home_env_file)...($env.ALGA_COLOR_RESET)"
|
|
let env_vars = (open $home_env_file
|
|
| lines
|
|
| each { |line| $line | str trim }
|
|
| where { |line| not ($line | str starts-with '#') and ($line | str length) > 0 and ($line | str contains '=') }
|
|
| split column "=" -n 2
|
|
| rename key value
|
|
| update key {|it| $it.key | str trim }
|
|
| update value {|it| if ($it.value | is-empty) { "" } else { $it.value | str trim | str trim -c '"' | str trim -c "'" } }
|
|
| reduce -f {} { |item, acc| $acc | upsert $item.key $item.value })
|
|
|
|
# Load all variables from .env, overwriting existing ones from the environment
|
|
load-env $env_vars
|
|
}
|
|
|
|
# Get LLM configuration from environment variables
|
|
let custom_openai_api_key = ($env.CUSTOM_OPENAI_API_KEY? | default "")
|
|
let custom_openai_base_url = ($env.CUSTOM_OPENAI_BASE_URL? | default "https://openrouter.ai/api/v1")
|
|
let custom_openai_model = ($env.CUSTOM_OPENAI_MODEL? | default "google/gemini-2.5-flash-preview-05-20")
|
|
|
|
# Show warning if API key is not set
|
|
if ($custom_openai_api_key | str length) == 0 {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: CUSTOM_OPENAI_API_KEY environment variable not set. AI automation may not work.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Set the environment variable in your .env file or export CUSTOM_OPENAI_API_KEY=your-key-here($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
# Load configuration defaults
|
|
let config = load-config
|
|
|
|
# Use provided values, fall back to config, then to defaults
|
|
let git_author_name = if ($author_name | str length) > 0 {
|
|
$author_name
|
|
} else if ($config.dev_env.author.name | str length) > 0 {
|
|
$config.dev_env.author.name
|
|
} else {
|
|
"Dev Environment"
|
|
}
|
|
|
|
let git_author_email = if ($author_email | str length) > 0 {
|
|
$author_email
|
|
} else if ($config.dev_env.author.email | str length) > 0 {
|
|
$config.dev_env.author.email
|
|
} else {
|
|
"dev@alga.local"
|
|
}
|
|
|
|
# Show what author info is being used
|
|
if ($config.dev_env.author.name | str length) > 0 and ($author_name | str length) == 0 {
|
|
print $"($env.ALGA_COLOR_CYAN)Using git author from config: ($git_author_name) <($git_author_email)>($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
let values_content = $"
|
|
# Generated values for branch ($branch) development environment
|
|
devEnv:
|
|
enabled: true
|
|
branch: \"($branch)\"
|
|
sanitizedBranch: \"($sanitized_branch)\"
|
|
namespace: \"($namespace)\"
|
|
repository:
|
|
url: \"https://github.com/Nine-Minds/alga-psa.git\"
|
|
branch: \"($branch)\"
|
|
git:
|
|
authorName: \"($git_author_name)\"
|
|
authorEmail: \"($git_author_email)\"
|
|
codeServer:
|
|
enabled: true
|
|
aiAutomation:
|
|
enabled: true
|
|
# External port configuration
|
|
externalPorts:
|
|
app: ($app_port)
|
|
codeServer: ($code_server_port)
|
|
codeApp: ($code_app_port)
|
|
aiWeb: ($ai_web_port)
|
|
|
|
server:
|
|
image:
|
|
name: \"($image_name)\"
|
|
tag: \"($image_tag)\"
|
|
pullPolicy: Always # Force pull to avoid cache issues
|
|
|
|
# LLM Configuration
|
|
config:
|
|
llm:
|
|
customOpenaiApiKey: \"($custom_openai_api_key)\"
|
|
customOpenaiBaseUrl: \"($custom_openai_base_url)\"
|
|
customOpenaiModel: \"($custom_openai_model)\"
|
|
|
|
($edition_comment)"
|
|
|
|
# Write temporary values file
|
|
$values_content | save -f $temp_values_file
|
|
|
|
try {
|
|
# Deploy using Helm (namespace will be created by template)
|
|
print $"($env.ALGA_COLOR_CYAN)Deploying Helm chart...($env.ALGA_COLOR_RESET)"
|
|
let helm_result = do {
|
|
cd $project_root
|
|
helm upgrade --install $"alga-dev-($sanitized_branch)" ./helm -f helm/values-dev-env.yaml -f $temp_values_file -n $namespace --create-namespace | complete
|
|
}
|
|
|
|
# Check for actual deployment failures (ignore benign warnings/messages)
|
|
let has_real_error = if $helm_result.exit_code != 0 {
|
|
# Check if stderr contains actual errors vs just warnings
|
|
let stderr_content = ($helm_result.stderr | str downcase)
|
|
let is_namespace_exists_only = ($stderr_content | str contains 'already exists')
|
|
let has_warnings = ($stderr_content | str contains 'warning:')
|
|
let has_errors = ($stderr_content | str contains 'error:')
|
|
let is_warnings_only = ($has_warnings and not $has_errors)
|
|
|
|
# Only treat as real error if it's not just namespace exists or warnings
|
|
not ($is_namespace_exists_only or $is_warnings_only)
|
|
} else { false }
|
|
|
|
if $has_real_error {
|
|
print $"($env.ALGA_COLOR_RED)Helm deployment failed:($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_RED)($helm_result.stderr)($env.ALGA_COLOR_RESET)"
|
|
let error_msg = $"Failed to deploy development environment: ($helm_result.stderr)"
|
|
error make { msg: $"($env.ALGA_COLOR_RED)($error_msg)($env.ALGA_COLOR_RESET)", code: $helm_result.exit_code }
|
|
} else if $helm_result.exit_code != 0 {
|
|
# Helm deployment had issues but resources are deployed - try upgrade to trigger hooks
|
|
print $"($env.ALGA_COLOR_YELLOW)Initial deployment had issues, attempting upgrade to ensure hooks run...($env.ALGA_COLOR_RESET)"
|
|
let upgrade_result = do {
|
|
cd $project_root
|
|
helm upgrade $"alga-dev-($sanitized_branch)" ./helm -f helm/values-dev-env.yaml -f $temp_values_file -n $namespace | complete
|
|
}
|
|
|
|
if $upgrade_result.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Upgrade successful - hooks should have run for database initialization.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Upgrade also had issues. Database may not be initialized.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
|
|
# Show warnings but don't treat as errors
|
|
if $helm_result.exit_code != 0 and not $has_real_error {
|
|
print $"($env.ALGA_COLOR_YELLOW)Helm completed with warnings - ignoring:($env.ALGA_COLOR_RESET)"
|
|
# Filter out the file permission warnings which are just noise
|
|
let filtered_stderr = ($helm_result.stderr | lines | where { |line|
|
|
(not ($line | str contains 'Kubernetes configuration file is'))
|
|
} | where { |line|
|
|
(not ($line | str contains 'deprecated since'))
|
|
} | where { |line|
|
|
(($line | str trim | str length) > 0)
|
|
})
|
|
if ($filtered_stderr | length) > 0 {
|
|
$filtered_stderr | each { |line| print $" ($line)" }
|
|
}
|
|
}
|
|
|
|
print $helm_result.stdout
|
|
print $"($env.ALGA_COLOR_GREEN)Helm deployment completed successfully.($env.ALGA_COLOR_RESET)"
|
|
|
|
# Wait for deployments to be ready
|
|
print $"($env.ALGA_COLOR_CYAN)Waiting for deployments to be ready...($env.ALGA_COLOR_RESET)"
|
|
let wait_result = do {
|
|
kubectl wait --for=condition=available --timeout=300s deployment -l app.kubernetes.io/instance=$"alga-dev-($sanitized_branch)" -n $namespace | complete
|
|
}
|
|
|
|
if $wait_result.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)All deployments are ready!($env.ALGA_COLOR_RESET)"
|
|
|
|
# Show environment status
|
|
dev-env-status $branch
|
|
} else {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Some deployments may still be starting. Use 'dev-env-status ($branch)' to check progress.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
} catch { |err|
|
|
print $"($env.ALGA_COLOR_RED)Error during deployment: ($err)($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
# Clean up temporary files
|
|
if ($temp_values_file | path exists) {
|
|
rm $temp_values_file
|
|
}
|
|
}
|
|
|
|
# List active development environments
|
|
export def dev-env-list [] {
|
|
print $"($env.ALGA_COLOR_CYAN)Active development environments:($env.ALGA_COLOR_RESET)"
|
|
|
|
let namespaces_result = do {
|
|
kubectl get namespaces -l type=dev-environment -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.branch}{"\n"}{end}' | complete
|
|
}
|
|
|
|
if $namespaces_result.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_RED)Failed to list environments: ($namespaces_result.stderr)($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
let environments = ($namespaces_result.stdout | lines | where ($it | str trim | str length) > 0)
|
|
|
|
if ($environments | length) == 0 {
|
|
print $"($env.ALGA_COLOR_YELLOW)No active development environments found.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
print "┌──────────────────────────────────────────────────────────────────────────┐"
|
|
print "│ Namespace │ Branch │ Status │"
|
|
print "├──────────────────────────────────────────────────────────────────────────┤"
|
|
|
|
for env_line in $environments {
|
|
let parts = ($env_line | split column "\t")
|
|
let namespace = ($parts | get column1 | get 0)
|
|
let branch = ($parts | get column2? | get 0? | default "Unknown")
|
|
let status_result = do {
|
|
kubectl get deployments -n $namespace -o jsonpath='{range .items[*]}{.status.readyReplicas}{" "}{.status.replicas}{"\n"}{end}' | complete
|
|
}
|
|
|
|
let status = if $status_result.exit_code == 0 {
|
|
let ready_total = ($status_result.stdout | lines | each { |line|
|
|
if ($line | str trim | str length) > 0 {
|
|
let parts = ($line | split row " ")
|
|
let ready = ($parts | get 0? | default "0" | if ($in == "") { "0" } else { $in } | into int)
|
|
let total = ($parts | get 1? | default "0" | if ($in == "") { "0" } else { $in } | into int)
|
|
{ ready: $ready, total: $total }
|
|
}
|
|
} | compact)
|
|
|
|
let total_ready = if ($ready_total | is-empty) { 0 } else { ($ready_total | get ready | math sum) }
|
|
let total_deployments = if ($ready_total | is-empty) { 0 } else { ($ready_total | get total | math sum) }
|
|
|
|
if $total_ready == $total_deployments {
|
|
"Ready"
|
|
} else {
|
|
$"($total_ready)/($total_deployments) Ready"
|
|
}
|
|
} else {
|
|
"Error"
|
|
}
|
|
|
|
print $"│ ($namespace | fill -w 24) │ ($branch | fill -w 20) │ ($status | fill -w 22) │"
|
|
}
|
|
|
|
print "└──────────────────────────────────────────────────────────────────────────┘"
|
|
}
|
|
|
|
# Connect to development environment
|
|
export def dev-env-connect [
|
|
branch: string # Branch name to connect to
|
|
] {
|
|
# Sanitize branch name for namespace lookup
|
|
let sanitized_branch = (sanitize-branch-name $branch)
|
|
let namespace = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Check if environment exists
|
|
let env_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $env_check.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_RED)Environment for branch ($branch) not found.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-list' to see available environments.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Connecting to development environment for branch: ($branch)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Setting up port forwarding...($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)This will run in foreground. Press Ctrl+C to stop.($env.ALGA_COLOR_RESET)"
|
|
|
|
# Find available ports dynamically at connect time
|
|
print $"($env.ALGA_COLOR_CYAN)Finding available ports for port forwarding...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Function to find a free port (same as in dev-env-create)
|
|
def find-free-port [start_port: int] {
|
|
mut port = $start_port
|
|
mut found = false
|
|
|
|
while not $found and $port < 65535 {
|
|
# Copy mutable variable to avoid capture issue
|
|
let current_port = $port
|
|
|
|
# Check if port is in use
|
|
let check_result = do {
|
|
bash -c $"nc -z localhost ($current_port) 2>/dev/null" | complete
|
|
}
|
|
|
|
if $check_result.exit_code != 0 {
|
|
# Port is free
|
|
$found = true
|
|
} else {
|
|
$port = $port + 1
|
|
}
|
|
}
|
|
|
|
if $found { $port } else { 0 }
|
|
}
|
|
|
|
# Find ports for each service
|
|
let app_port = find-free-port 30000
|
|
let code_server_port = find-free-port ($app_port + 1)
|
|
let code_app_port = find-free-port ($code_server_port + 1)
|
|
let ai_web_port = find-free-port ($code_app_port + 1)
|
|
|
|
if $app_port == 0 or $code_server_port == 0 or $code_app_port == 0 or $ai_web_port == 0 {
|
|
print $"($env.ALGA_COLOR_RED)Could not find available ports for services($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_GREEN)Found available ports:($env.ALGA_COLOR_RESET)"
|
|
print $" Main App: ($app_port)"
|
|
print $" Code Server: ($code_server_port)"
|
|
print $" Code App: ($code_app_port)"
|
|
print $" AI Web: ($ai_web_port)"
|
|
|
|
# Start port forwarding processes with found ports
|
|
print $"($env.ALGA_COLOR_CYAN)Starting port forwarding processes...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Start processes using bash for proper backgrounding with specific ports
|
|
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-code-server --address=127.0.0.1 ($code_server_port):8080 > /tmp/pf-code-server-($sanitized_branch).log 2>&1 &"
|
|
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch) --address=127.0.0.1 ($app_port):3000 > /tmp/pf-main-app-($sanitized_branch).log 2>&1 &"
|
|
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-code-server --address=127.0.0.1 ($code_app_port):3000 > /tmp/pf-code-app-($sanitized_branch).log 2>&1 &"
|
|
bash -c $"kubectl port-forward -n ($namespace) svc/alga-dev-($sanitized_branch)-ai-nginx --address=127.0.0.1 ($ai_web_port):8080 > /tmp/pf-ai-web-($sanitized_branch).log 2>&1 &"
|
|
|
|
# Give processes time to start
|
|
sleep 2sec
|
|
|
|
# Check if port forwarding started successfully
|
|
let pf_check = do {
|
|
bash -c $"ps aux | grep -E 'kubectl port-forward.*alga-dev-($sanitized_branch)' | grep -v grep | wc -l" | complete
|
|
}
|
|
|
|
if ($pf_check.stdout | str trim | into int) < 4 {
|
|
print $"($env.ALGA_COLOR_YELLOW)Warning: Some port forwarding processes may not have started properly($env.ALGA_COLOR_RESET)"
|
|
print "Checking logs..."
|
|
|
|
# Show any errors from log files
|
|
for log_file in [
|
|
$"/tmp/pf-code-server-($sanitized_branch).log"
|
|
$"/tmp/pf-main-app-($sanitized_branch).log"
|
|
$"/tmp/pf-code-app-($sanitized_branch).log"
|
|
$"/tmp/pf-ai-web-($sanitized_branch).log"
|
|
] {
|
|
if ($log_file | path exists) {
|
|
let content = (open $log_file)
|
|
if ($content | str contains "error") {
|
|
print $"($env.ALGA_COLOR_RED)Errors in ($log_file):($env.ALGA_COLOR_RESET)"
|
|
print $content
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Display the URLs
|
|
print $"($env.ALGA_COLOR_CYAN)Port forwarding setup:($env.ALGA_COLOR_RESET)"
|
|
print $" Code Server: http://localhost:($code_server_port)"
|
|
print $" Password: alga-dev"
|
|
print $" PSA App \(main\): http://localhost:($app_port)"
|
|
print $" PSA App \(in code\): http://localhost:($code_app_port)"
|
|
print $" AI Web: http://localhost:($ai_web_port)"
|
|
|
|
# Update NEXTAUTH_URL in .env files for main app pods only (code server uses hardcoded internal URL)
|
|
print $"($env.ALGA_COLOR_CYAN)Configuring NEXTAUTH_URL in main app .env files...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Helper function to update .env file
|
|
def update-env-file [pod_name: string, nextauth_url: string, description: string] {
|
|
print $" Updating ($description): ($pod_name)"
|
|
|
|
# Check if NEXTAUTH_URL already exists in .env file
|
|
let env_check = do {
|
|
kubectl exec -n $namespace $pod_name -- grep -E "^NEXTAUTH_URL=" .env 2>/dev/null | complete
|
|
}
|
|
|
|
if $env_check.exit_code == 0 and ($env_check.stdout | str trim | str length) > 0 {
|
|
# NEXTAUTH_URL exists, check if it's different
|
|
let current_url = ($env_check.stdout | str trim | split column "=" | get column2.0)
|
|
|
|
if $current_url != $nextauth_url {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Warning: .env already contains NEXTAUTH_URL=($current_url)($env.ALGA_COLOR_RESET)"
|
|
print $" ($env.ALGA_COLOR_YELLOW)This may indicate another developer is using this environment.($env.ALGA_COLOR_RESET)"
|
|
print $" ($env.ALGA_COLOR_YELLOW)Updating to your port configuration: ($nextauth_url)($env.ALGA_COLOR_RESET)"
|
|
|
|
# Update existing entry
|
|
kubectl exec -n $namespace $pod_name -- sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=($nextauth_url)|" .env | complete
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_GREEN)NEXTAUTH_URL already correctly set to ($nextauth_url)($env.ALGA_COLOR_RESET)"
|
|
}
|
|
} else {
|
|
# NEXTAUTH_URL doesn't exist, add it
|
|
print $" ($env.ALGA_COLOR_CYAN)Adding NEXTAUTH_URL=($nextauth_url) to .env file($env.ALGA_COLOR_RESET)"
|
|
kubectl exec -n $namespace $pod_name -- sh -c $"echo 'NEXTAUTH_URL=($nextauth_url)' >> .env" | complete
|
|
}
|
|
}
|
|
|
|
# Get main app pod names and update their .env files (skip code server - it uses hardcoded internal URL)
|
|
let main_app_pods = do {
|
|
kubectl get pods -n $namespace -l "app.kubernetes.io/component!=code-server,app.kubernetes.io/component!=ai-automation-api,app.kubernetes.io/component!=ai-automation-web" -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
# Update main app pods only
|
|
if $main_app_pods.exit_code == 0 and not ($main_app_pods.stdout | is-empty) {
|
|
let main_pods = ($main_app_pods.stdout | str trim | split row ' ')
|
|
for pod in $main_pods {
|
|
if ($pod | str trim | str length) > 0 {
|
|
update-env-file $pod $"http://localhost:($app_port)" "main app"
|
|
}
|
|
}
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_GREEN)NEXTAUTH_URL configuration completed.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_CYAN)Note: Code server uses hardcoded internal URL \(http://code-server:3000\) for browser testing within the environment.($env.ALGA_COLOR_RESET)"
|
|
|
|
print $"($env.ALGA_COLOR_GREEN)Port forwarding active!($env.ALGA_COLOR_RESET)"
|
|
|
|
# Wait for user to stop
|
|
input "Press Enter to stop port forwarding..."
|
|
|
|
# Kill all kubectl port-forward processes
|
|
bash -c $"pkill -f 'kubectl port-forward.*alga-dev-($sanitized_branch)'"
|
|
|
|
# Clean up log files
|
|
rm -f $"/tmp/pf-code-server-($sanitized_branch).log"
|
|
rm -f $"/tmp/pf-main-app-($sanitized_branch).log"
|
|
rm -f $"/tmp/pf-code-app-($sanitized_branch).log"
|
|
rm -f $"/tmp/pf-ai-web-($sanitized_branch).log"
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Port forwarding stopped.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
# Destroy development environment
|
|
export def dev-env-destroy [
|
|
branch: string # Branch name to destroy
|
|
--force # Force deletion without confirmation
|
|
] {
|
|
# Sanitize branch name for namespace lookup
|
|
let sanitized_branch = (sanitize-branch-name $branch)
|
|
let namespace = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Helper to get all ai-api pods in the namespace
|
|
def get-ai-pods [] {
|
|
let pods_by_label = do {
|
|
kubectl get pods -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
let pods_by_name = do {
|
|
kubectl get pods -n $namespace -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
let pods1 = if $pods_by_label.exit_code == 0 and not ($pods_by_label.stdout | is-empty) {
|
|
$pods_by_label.stdout | str trim | split row ' '
|
|
} else {
|
|
[]
|
|
}
|
|
|
|
let pods2 = if $pods_by_name.exit_code == 0 and not ($pods_by_name.stdout | is-empty) {
|
|
$pods_by_name.stdout | str trim | split row ' ' | where {|it| $it | str contains "ai-api"}
|
|
} else {
|
|
[]
|
|
}
|
|
|
|
$pods1 | append $pods2 | uniq
|
|
}
|
|
|
|
# Check if environment exists
|
|
let env_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $env_check.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_YELLOW)Environment for branch ($branch) not found or already destroyed.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
if not $force {
|
|
print $"($env.ALGA_COLOR_YELLOW)This will permanently destroy the development environment for branch ($branch).($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)All data in the environment will be lost.($env.ALGA_COLOR_RESET)"
|
|
let confirmation = (input $"Type 'yes' to confirm destruction: ")
|
|
|
|
if $confirmation != "yes" {
|
|
print $"($env.ALGA_COLOR_CYAN)Destruction cancelled.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Destroying development environment for branch: ($branch)...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Step 1: Kill any stuck hook jobs first
|
|
print $"($env.ALGA_COLOR_CYAN)1. Cleaning up stuck hook jobs...($env.ALGA_COLOR_RESET)"
|
|
let stuck_jobs = do {
|
|
kubectl get jobs -n $namespace -o jsonpath='{.items[*].metadata.name}' | complete
|
|
}
|
|
|
|
if $stuck_jobs.exit_code == 0 and ($stuck_jobs.stdout | str trim | str length) > 0 {
|
|
let job_names = ($stuck_jobs.stdout | str trim | split row ' ')
|
|
for job in $job_names {
|
|
if ($job | str trim | str length) > 0 {
|
|
print $" Deleting job: ($job)"
|
|
kubectl delete job $job -n $namespace --timeout=10s --force --grace-period=0 | complete
|
|
}
|
|
}
|
|
} else {
|
|
print $" No stuck jobs found"
|
|
}
|
|
|
|
# Step 2: Force stop ai-api pods specifically (known to cause stuck namespaces)
|
|
# Step 2: Scale down and remove ai-api resources to prevent them from getting stuck
|
|
print $"($env.ALGA_COLOR_CYAN)2. Scaling down ai-api deployment...($env.ALGA_COLOR_RESET)"
|
|
let ai_api_deployments = do {
|
|
kubectl get deployment -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
if $ai_api_deployments.exit_code == 0 and not ($ai_api_deployments.stdout | is-empty) {
|
|
let deployment_names = ($ai_api_deployments.stdout | str trim | split row ' ')
|
|
for deployment in $deployment_names {
|
|
if ($deployment | str trim | str length) > 0 {
|
|
print $" Scaling down deployment: ($deployment)"
|
|
kubectl scale deployment $deployment --replicas=0 -n $namespace --timeout=30s | complete
|
|
}
|
|
}
|
|
} else {
|
|
print $" No ai-api deployment found to scale down."
|
|
}
|
|
|
|
# Now, wait for the pods to terminate
|
|
print $"($env.ALGA_COLOR_CYAN)Waiting for ai-api pods to terminate...($env.ALGA_COLOR_RESET)"
|
|
mut wait_retries = 0
|
|
while $wait_retries < 30 { # Wait for up to 60 seconds
|
|
let remaining_pods = (get-ai-pods)
|
|
if ($remaining_pods | is-empty) {
|
|
print $"\n($env.ALGA_COLOR_GREEN)All targeted ai-api pods have been terminated.($env.ALGA_COLOR_RESET)"
|
|
break
|
|
} else {
|
|
let remaining_str = ($remaining_pods | str join ", ")
|
|
print -n $"\r Waiting... remaining: ($remaining_str)"
|
|
sleep 2sec
|
|
$wait_retries = $wait_retries + 1
|
|
}
|
|
}
|
|
if $wait_retries >= 30 {
|
|
print $"\n($env.ALGA_COLOR_YELLOW)Warning: Pods did not terminate gracefully. Forcing deletion...($env.ALGA_COLOR_RESET)"
|
|
let remaining_pods = (get-ai-pods)
|
|
if not ($remaining_pods | is-empty) {
|
|
for pod in $remaining_pods {
|
|
if ($pod | str trim | str length) > 0 {
|
|
print $" Force deleting pod: ($pod)"
|
|
kubectl delete pod $pod -n $namespace --force --grace-period=0 | complete
|
|
}
|
|
}
|
|
sleep 5sec # Give it a moment after force deletion
|
|
}
|
|
}
|
|
|
|
# Step 3: Check for other stuck resources and handle them
|
|
print $"($env.ALGA_COLOR_CYAN)3. Checking for other stuck resources...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Check for stuck pods
|
|
let stuck_pods = do {
|
|
kubectl get pods -n $namespace --field-selector=status.phase!=Running,status.phase!=Succeeded | complete
|
|
}
|
|
let has_terminating = ($stuck_pods.stdout | str contains 'Terminating')
|
|
let has_pending = ($stuck_pods.stdout | str contains 'Pending')
|
|
if $stuck_pods.exit_code == 0 and ($has_terminating or $has_pending) {
|
|
print $" Found stuck pods, force deleting..."
|
|
kubectl delete pods --all -n $namespace --force --grace-period=0 | complete
|
|
}
|
|
|
|
# Step 4: Remove PV finalizers if stuck
|
|
print $"($env.ALGA_COLOR_CYAN)4. Checking for stuck persistent volumes...($env.ALGA_COLOR_RESET)"
|
|
let stuck_pvs = do {
|
|
kubectl get pv | grep $namespace | awk '{print $1}' | complete
|
|
}
|
|
|
|
if $stuck_pvs.exit_code == 0 and ($stuck_pvs.stdout | str trim | str length) > 0 {
|
|
let pv_names = ($stuck_pvs.stdout | lines | where { |line| ($line | str trim | str length) > 0 })
|
|
for pv_name in $pv_names {
|
|
print $" Checking PV finalizers: ($pv_name)"
|
|
# Try to patch finalizers to empty array to unstick the PV
|
|
kubectl patch pv ($pv_name | str trim) -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
|
|
}
|
|
} else {
|
|
print $" No stuck persistent volumes found"
|
|
}
|
|
|
|
# Step 5: Find and remove Helm release - check where it actually exists
|
|
print $"($env.ALGA_COLOR_CYAN)5. Locating and removing Helm release...($env.ALGA_COLOR_RESET)"
|
|
let release_name = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Check if release exists in the environment namespace
|
|
let helm_check_ns = do {
|
|
helm status $release_name -n $namespace | complete
|
|
}
|
|
|
|
# Check if release exists in default namespace
|
|
let helm_check_default = do {
|
|
helm status $release_name -n default | complete
|
|
}
|
|
|
|
if $helm_check_ns.exit_code == 0 {
|
|
print $" Found release in ($namespace), removing..."
|
|
let helm_result = do {
|
|
helm uninstall $release_name -n $namespace --timeout=60s --no-hooks --cascade=background | complete
|
|
}
|
|
|
|
if $helm_result.exit_code == 0 {
|
|
print $" ($env.ALGA_COLOR_GREEN)Helm release removed successfully from ($namespace).($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Warning: Helm uninstall had issues, trying force cleanup...($env.ALGA_COLOR_RESET)"
|
|
# Force delete the release by removing finalizers
|
|
helm uninstall $release_name -n $namespace --timeout=30s --no-hooks | complete
|
|
}
|
|
} else if $helm_check_default.exit_code == 0 {
|
|
print $" Found release in default namespace, removing..."
|
|
let helm_result = do {
|
|
helm uninstall $release_name -n default --timeout=60s --no-hooks --cascade=background | complete
|
|
}
|
|
|
|
if $helm_result.exit_code == 0 {
|
|
print $" ($env.ALGA_COLOR_GREEN)Helm release removed successfully from default namespace.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Warning: Helm uninstall had issues, trying force cleanup...($env.ALGA_COLOR_RESET)"
|
|
# Force delete the release by removing finalizers
|
|
helm uninstall $release_name -n default --timeout=30s --no-hooks | complete
|
|
}
|
|
} else {
|
|
print $" No Helm release found for ($release_name) in either namespace."
|
|
}
|
|
|
|
# Step 6: Clean up remaining resources systematically
|
|
print $"($env.ALGA_COLOR_CYAN)6. Cleaning up remaining resources...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Delete all resources in the namespace first
|
|
print $" Deleting all workload resources..."
|
|
kubectl delete all --all -n $namespace --timeout=30s | complete
|
|
|
|
print $" Deleting persistent volume claims..."
|
|
kubectl delete pvc --all -n $namespace --timeout=30s | complete
|
|
|
|
print $" Deleting config and secrets..."
|
|
kubectl delete configmaps,secrets --all -n $namespace --timeout=30s | complete
|
|
|
|
print $" Deleting ingress resources..."
|
|
kubectl delete ingress --all -n $namespace --timeout=30s | complete
|
|
|
|
# Step 7: Force cleanup any remaining persistent volumes
|
|
print $"($env.ALGA_COLOR_CYAN)7. Force cleaning up persistent volumes...($env.ALGA_COLOR_RESET)"
|
|
let remaining_pv_list = do {
|
|
kubectl get pv | grep $namespace | awk '{print $1}' | complete
|
|
}
|
|
|
|
if $remaining_pv_list.exit_code == 0 and ($remaining_pv_list.stdout | str trim | str length) > 0 {
|
|
let pv_names = ($remaining_pv_list.stdout | lines | where { |line| ($line | str trim | str length) > 0 })
|
|
for pv_name in $pv_names {
|
|
print $" Force deleting PV: ($pv_name)"
|
|
# Remove finalizers first, then delete
|
|
kubectl patch pv ($pv_name | str trim) -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
|
|
kubectl delete pv ($pv_name | str trim) --timeout=10s --force --grace-period=0 | complete
|
|
}
|
|
} else {
|
|
print $" No remaining persistent volumes found"
|
|
}
|
|
|
|
# Step 8: Delete the namespace
|
|
print $"($env.ALGA_COLOR_CYAN)8. Deleting namespace...($env.ALGA_COLOR_RESET)"
|
|
|
|
# First try to patch out any finalizers on the namespace itself
|
|
print $" Removing namespace finalizers..."
|
|
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
|
|
|
|
# Short timeout for initial deletion attempt
|
|
let namespace_result = do {
|
|
kubectl delete namespace $namespace --timeout=30s | complete
|
|
}
|
|
|
|
if $namespace_result.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Development environment for branch ($branch) destroyed successfully.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Warning: Standard namespace deletion had issues. Attempting force cleanup...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Wait a moment for any pending deletions to complete
|
|
sleep 2sec
|
|
|
|
# Check if namespace still exists
|
|
let ns_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $ns_check.exit_code != 0 {
|
|
print $" ($env.ALGA_COLOR_GREEN)Namespace was deleted during wait period.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" Namespace still exists, forcing deletion..."
|
|
# Final attempt to delete namespace with grace period 0 and shorter timeout
|
|
let force_namespace_result = do {
|
|
kubectl delete namespace $namespace --grace-period=0 --force --timeout=20s | complete
|
|
}
|
|
|
|
if $force_namespace_result.exit_code == 0 {
|
|
print $" ($env.ALGA_COLOR_GREEN)Namespace force deleted successfully.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Force delete timed out or failed. Checking if deletion is in progress...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Check final status
|
|
let final_ns_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $final_ns_check.exit_code != 0 {
|
|
print $" ($env.ALGA_COLOR_GREEN)Namespace deletion completed.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_RED)Namespace still exists. May require manual cleanup.($env.ALGA_COLOR_RESET)"
|
|
print $" ($env.ALGA_COLOR_YELLOW)The namespace may be stuck due to remaining finalizers.($env.ALGA_COLOR_RESET)"
|
|
print $" ($env.ALGA_COLOR_YELLOW)Try: kubectl patch namespace ($namespace) -p '{\\\"metadata\\\":{\\\"finalizers\\\":null}}' --type=merge($env.ALGA_COLOR_RESET)"
|
|
print $" ($env.ALGA_COLOR_YELLOW)Then: kubectl delete namespace ($namespace) --grace-period=0 --force($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Step 9: Final verification and cleanup
|
|
print $"($env.ALGA_COLOR_CYAN)9. Final verification...($env.ALGA_COLOR_RESET)"
|
|
let final_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $final_check.exit_code == 0 {
|
|
print $" ($env.ALGA_COLOR_YELLOW)Namespace still exists, attempting final cleanup...($env.ALGA_COLOR_RESET)"
|
|
# Last resort - try to remove any stuck finalizers on the namespace itself
|
|
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
|
|
kubectl delete namespace $namespace --grace-period=0 --force | complete
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_GREEN)Namespace successfully removed.($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
|
|
# Force cleanup stuck development environment resources
|
|
export def dev-env-force-cleanup [
|
|
branch: string # Branch name to force cleanup
|
|
] {
|
|
# Sanitize branch name for namespace lookup
|
|
let sanitized_branch = (sanitize-branch-name $branch)
|
|
let namespace = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Helper to get all ai-api pods in the namespace
|
|
def get-ai-pods [] {
|
|
let pods_by_label = do {
|
|
kubectl get pods -n $namespace -l 'app.kubernetes.io/component=ai-automation-api' -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
let pods_by_name = do {
|
|
kubectl get pods -n $namespace -o jsonpath='{.items[*].metadata.name}' --ignore-not-found | complete
|
|
}
|
|
|
|
let pods1 = if $pods_by_label.exit_code == 0 and not ($pods_by_label.stdout | is-empty) {
|
|
$pods_by_label.stdout | str trim | split row ' '
|
|
} else {
|
|
[]
|
|
}
|
|
|
|
let pods2 = if $pods_by_name.exit_code == 0 and not ($pods_by_name.stdout | is-empty) {
|
|
$pods_by_name.stdout | str trim | split row ' ' | where {|it| $it | str contains "ai-api"}
|
|
} else {
|
|
[]
|
|
}
|
|
|
|
$pods1 | append $pods2 | uniq
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Force cleaning up development environment for branch: ($branch)...($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)This will aggressively remove all resources and may take some time.($env.ALGA_COLOR_RESET)"
|
|
|
|
# Remove Helm release from both potential namespaces
|
|
print $"($env.ALGA_COLOR_CYAN)Removing Helm releases...($env.ALGA_COLOR_RESET)"
|
|
let release_name = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Check and remove from environment namespace
|
|
let helm_check_ns = do { helm status $release_name -n $namespace | complete }
|
|
if $helm_check_ns.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_CYAN)Removing release from ($namespace)...($env.ALGA_COLOR_RESET)"
|
|
helm uninstall $release_name -n $namespace | complete
|
|
}
|
|
|
|
# Check and remove from default namespace
|
|
let helm_check_default = do { helm status $release_name -n default | complete }
|
|
if $helm_check_default.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_CYAN)Removing release from default namespace...($env.ALGA_COLOR_RESET)"
|
|
helm uninstall $release_name -n default | complete
|
|
}
|
|
|
|
# Force stop ai-api pods first (known to cause stuck namespaces)
|
|
print $"($env.ALGA_COLOR_CYAN)Force stopping ai-api pods...($env.ALGA_COLOR_RESET)"
|
|
let pod_names_to_delete = (get-ai-pods)
|
|
|
|
if ($pod_names_to_delete | is-empty) {
|
|
print $" No ai-api pods found."
|
|
} else {
|
|
print $" Found pods to delete: ($pod_names_to_delete | str join ', ')"
|
|
for pod in $pod_names_to_delete {
|
|
if ($pod | str trim | str length) > 0 {
|
|
print $" Force deleting ai-api pod: ($pod)"
|
|
kubectl delete pod $pod -n $namespace --force --grace-period=0 | complete
|
|
}
|
|
}
|
|
|
|
# Wait for ai-api pods to be fully terminated
|
|
print $"($env.ALGA_COLOR_CYAN)Waiting for ai-api pods to terminate...($env.ALGA_COLOR_RESET)"
|
|
mut wait_retries = 0
|
|
while $wait_retries < 15 { # Wait for up to 30 seconds
|
|
let remaining_pods = (get-ai-pods)
|
|
if ($remaining_pods | is-empty) {
|
|
print $"\n($env.ALGA_COLOR_GREEN)All targeted ai-api pods have been terminated.($env.ALGA_COLOR_RESET)"
|
|
break
|
|
} else {
|
|
let remaining_text = ($remaining_pods | str join ', ')
|
|
print -n $"\r Waiting... remaining: ($remaining_text)"
|
|
sleep 2sec
|
|
$wait_retries = $wait_retries + 1
|
|
}
|
|
}
|
|
if $wait_retries >= 15 {
|
|
print $"\n($env.ALGA_COLOR_YELLOW)Warning: Some ai-api pods may not have terminated correctly. Continuing...($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
|
|
# Delete all resources in the namespace
|
|
print $"($env.ALGA_COLOR_CYAN)Deleting all namespace resources...($env.ALGA_COLOR_RESET)"
|
|
kubectl delete all --all -n $namespace --timeout=30s | complete
|
|
kubectl delete pvc --all -n $namespace --timeout=30s | complete
|
|
kubectl delete configmaps,secrets --all -n $namespace --timeout=30s | complete
|
|
kubectl delete ingress --all -n $namespace --timeout=30s | complete
|
|
|
|
# Remove finalizers from persistent volumes if they exist
|
|
print $"($env.ALGA_COLOR_CYAN)Checking for stuck persistent volumes...($env.ALGA_COLOR_RESET)"
|
|
let pvs_result = do {
|
|
kubectl get pv -o json | complete
|
|
}
|
|
|
|
if $pvs_result.exit_code == 0 {
|
|
# This would require jq to parse JSON properly, so we'll skip PV cleanup for now
|
|
print $"($env.ALGA_COLOR_YELLOW)Note: If PVs are stuck, you may need to manually remove finalizers($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
# Force delete the namespace
|
|
print $"($env.ALGA_COLOR_CYAN)Force deleting namespace...($env.ALGA_COLOR_RESET)"
|
|
|
|
# Remove namespace finalizers first
|
|
print $" Removing namespace finalizers..."
|
|
kubectl patch namespace $namespace -p '{\"metadata\":{\"finalizers\":null}}' --type=merge | complete
|
|
|
|
let namespace_result = do {
|
|
kubectl delete namespace $namespace --grace-period=0 --force --timeout=30s | complete
|
|
}
|
|
|
|
if $namespace_result.exit_code == 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Force cleanup completed successfully.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
# Wait and check if deletion completed
|
|
print $" Waiting for namespace deletion to complete..."
|
|
sleep 3sec
|
|
|
|
let final_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $final_check.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_GREEN)Namespace deletion completed after wait.($env.ALGA_COLOR_RESET)"
|
|
} else {
|
|
print $"($env.ALGA_COLOR_YELLOW)Some resources may still need manual cleanup.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Check with: kubectl get all -A | grep ($sanitized_branch)($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Or try: kubectl patch namespace ($namespace) -p '{\\\"metadata\\\":{\\\"finalizers\\\":null}}' --type=merge($env.ALGA_COLOR_RESET)"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Get environment status and URLs
|
|
export def dev-env-status [
|
|
branch?: string # Optional branch name, shows all if omitted
|
|
] {
|
|
if ($branch | is-empty) {
|
|
dev-env-list
|
|
return
|
|
}
|
|
|
|
# Sanitize branch name for namespace lookup
|
|
let sanitized_branch = (sanitize-branch-name $branch)
|
|
let namespace = $"alga-dev-($sanitized_branch)"
|
|
|
|
# Check if environment exists
|
|
let env_check = do {
|
|
kubectl get namespace $namespace | complete
|
|
}
|
|
|
|
if $env_check.exit_code != 0 {
|
|
print $"($env.ALGA_COLOR_RED)Environment for branch ($branch) not found.($env.ALGA_COLOR_RESET)"
|
|
print $"($env.ALGA_COLOR_YELLOW)Use 'dev-env-list' to see available environments.($env.ALGA_COLOR_RESET)"
|
|
return
|
|
}
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Development Environment Status - Branch: ($branch)($env.ALGA_COLOR_RESET)"
|
|
print "═══════════════════════════════════════════════════════"
|
|
|
|
# Get deployment status
|
|
print $"($env.ALGA_COLOR_CYAN)Deployments:($env.ALGA_COLOR_RESET)"
|
|
let deployments_result = do {
|
|
kubectl get deployments -n $namespace -o custom-columns="NAME:.metadata.name,READY:.status.readyReplicas,TOTAL:.status.replicas,AGE:.metadata.creationTimestamp" --no-headers | complete
|
|
}
|
|
|
|
if $deployments_result.exit_code == 0 {
|
|
($deployments_result.stdout | lines | each { |line|
|
|
if ($line | str trim | str length) > 0 {
|
|
print $" ($line)"
|
|
}
|
|
})
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_RED)Error getting deployment status($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
print ""
|
|
|
|
# Get service URLs
|
|
print $"($env.ALGA_COLOR_CYAN)Service URLs:($env.ALGA_COLOR_RESET)"
|
|
let ingress_result = do {
|
|
kubectl get ingress -n $namespace -o jsonpath='{range .items[*]}{range .spec.rules[*]}{.host}{"\n"}{end}{end}' | complete
|
|
}
|
|
|
|
if $ingress_result.exit_code == 0 {
|
|
let hosts = ($ingress_result.stdout | lines | each { |line| $line | str trim } | where { |x| ($x | str length) > 0 })
|
|
|
|
for host in $hosts {
|
|
let url = $"https://($host)"
|
|
if ($host | str contains "code") {
|
|
print $" Code Server: ($url)"
|
|
} else if ($host | str contains "ai-api") {
|
|
print $" AI API: ($url)"
|
|
} else if ($host | str contains "ai") {
|
|
print $" AI Web: ($url)"
|
|
} else {
|
|
print $" PSA App: ($url)"
|
|
}
|
|
}
|
|
} else {
|
|
print $" ($env.ALGA_COLOR_YELLOW)No ingress URLs found($env.ALGA_COLOR_RESET)"
|
|
}
|
|
|
|
print ""
|
|
|
|
# Get external ports if available
|
|
let ports_result = do {
|
|
kubectl get configmap -n $namespace $"alga-dev-($sanitized_branch)-external-ports" -o json | complete
|
|
}
|
|
|
|
if $ports_result.exit_code == 0 {
|
|
# Parse the ConfigMap data
|
|
let configmap_data = ($ports_result.stdout | from json)
|
|
let ports_data = $configmap_data.data
|
|
|
|
print $"($env.ALGA_COLOR_CYAN)Assigned External Ports:($env.ALGA_COLOR_RESET)"
|
|
print $" Main App: localhost:($ports_data.app)"
|
|
print $" Code Server: localhost:($ports_data.codeServer)"
|
|
print $" Code App: localhost:($ports_data.codeApp)"
|
|
print $" AI Web: localhost:($ports_data.aiWeb)"
|
|
print ""
|
|
}
|
|
|
|
# Port forward instructions
|
|
print $"($env.ALGA_COLOR_CYAN)Local Access - Port Forward:($env.ALGA_COLOR_RESET)"
|
|
print $" Run: dev-env-connect ($branch)"
|
|
print $" This will use the pre-assigned ports shown above"
|
|
|
|
print ""
|
|
print $"($env.ALGA_COLOR_CYAN)Management Commands:($env.ALGA_COLOR_RESET)"
|
|
print $" Connect: dev-env-connect ($branch)"
|
|
print $" Destroy: dev-env-destroy ($branch) [--force]"
|
|
}
|