param( [string]$EnvPath = (Join-Path $PSScriptRoot "..\Server\devtools\deployments\qsfera_full\.env"), [switch]$AllowWarnings ) $ErrorActionPreference = "Stop" $errors = New-Object System.Collections.Generic.List[string] $warnings = New-Object System.Collections.Generic.List[string] function Add-Error([string]$Message) { [void]$script:errors.Add($Message) Write-Error $Message -ErrorAction Continue } function Add-WarningMessage([string]$Message) { [void]$script:warnings.Add($Message) Write-Warning $Message } function Read-EnvFile([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { throw "Env file not found: $Path" } $result = @{} foreach ($line in Get-Content -LiteralPath $Path) { $trimmed = $line.Trim() if ($trimmed.Length -eq 0 -or $trimmed.StartsWith("#")) { continue } $separator = $trimmed.IndexOf("=") if ($separator -le 0) { continue } $key = $trimmed.Substring(0, $separator).Trim() $value = $trimmed.Substring($separator + 1).Trim() $value = $value.Trim('"').Trim("'") $result[$key] = $value } return $result } function Get-EnvValue([hashtable]$Values, [string]$Key) { if ($Values.ContainsKey($Key)) { return [string]$Values[$Key] } return "" } function Test-Placeholder([string]$Value) { if ([string]::IsNullOrWhiteSpace($Value)) { return $true } if ($Value.StartsWith("<") -and $Value.EndsWith(">")) { return $true } return $Value -in @("change-me", "changeme", "password", "secret") -or $Value.Contains("example.com") } function Test-Enabled([hashtable]$Values, [string]$Key) { return -not [string]::IsNullOrWhiteSpace((Get-EnvValue $Values $Key)) } function Require-Value([hashtable]$Values, [string]$Key, [string]$Label = $Key) { $value = Get-EnvValue $Values $Key if (Test-Placeholder $value) { Add-Error "$Label must be set to a production value." } } function Require-Secret([hashtable]$Values, [string]$Key) { $value = Get-EnvValue $Values $Key if (Test-Placeholder $value) { Add-Error "$Key must be set." return } if ($value -in @("admin", "demo", "keycloak", "qsfera", "qsfera-secret-key")) { Add-Error "$Key uses a known demo/default value." return } if ($value.Length -lt 16) { Add-Error "$Key must be at least 16 characters." } } function Require-ExactValue([hashtable]$Values, [string]$Key, [string]$ExpectedValue) { $value = Get-EnvValue $Values $Key if ($value -ne $ExpectedValue) { Add-Error "$Key must be $ExpectedValue." } } function Test-ComposeFileIncludes([string]$ComposeFile, [string]$EnvKey, [string]$Fragment) { if ([string]::IsNullOrWhiteSpace($ComposeFile)) { return $false } return $ComposeFile.Contains($Fragment) -or $ComposeFile.Contains('${' + $EnvKey + ':-}') } $resolvedEnvPath = (Resolve-Path -LiteralPath $EnvPath -ErrorAction Stop).Path $values = Read-EnvFile $resolvedEnvPath Require-Value $values "OC_DOMAIN" Require-Value $values "TRAEFIK_ACME_MAIL" Require-Secret $values "ADMIN_PASSWORD" if ((Get-EnvValue $values "INSECURE") -in @("true", "TRUE", "1", "yes", "YES")) { Add-Error "INSECURE must be false for production." } if ((Get-EnvValue $values "DEMO_USERS") -in @("true", "TRUE", "1", "yes", "YES")) { Add-Error "DEMO_USERS must be false for production." } switch (Get-EnvValue $values "OC_DOCKER_IMAGE") { "qsfera/qsfera" { } "" { Add-WarningMessage "OC_DOCKER_IMAGE is empty; compose default must be verified before rollout." } default { Add-Error "OC_DOCKER_IMAGE must be qsfera/qsfera for production." } } $dockerTag = Get-EnvValue $values "OC_DOCKER_TAG" if ([string]::IsNullOrWhiteSpace($dockerTag) -or $dockerTag -eq "latest" -or ($dockerTag.StartsWith("<") -and $dockerTag.EndsWith(">"))) { Add-Error "OC_DOCKER_TAG must pin a concrete production release tag." } Require-ExactValue $values "COMPOSE_PATH_SEPARATOR" ":" $composeFile = Get-EnvValue $values "COMPOSE_FILE" if ([string]::IsNullOrWhiteSpace($composeFile) -or -not $composeFile.Contains("docker-compose.yml")) { Add-Error "COMPOSE_FILE must include docker-compose.yml." } if (-not (Test-Enabled $values "QSFERA")) { Add-Error "QSFERA=:qsfera.yml must be enabled." } else { Require-ExactValue $values "QSFERA" ":qsfera.yml" if (-not (Test-ComposeFileIncludes $composeFile "QSFERA" "qsfera.yml")) { Add-Error "COMPOSE_FILE must include qsfera.yml or the `${QSFERA:-} expansion." } } if (Test-Enabled $values "DECOMPOSEDS3") { Require-ExactValue $values "DECOMPOSEDS3" ":decomposeds3.yml" if (-not (Test-ComposeFileIncludes $composeFile "DECOMPOSEDS3" "decomposeds3.yml")) { Add-Error "COMPOSE_FILE must include decomposeds3.yml or the `${DECOMPOSEDS3:-} expansion when DECOMPOSEDS3 is enabled." } Require-Value $values "DECOMPOSEDS3_ENDPOINT" Require-Value $values "DECOMPOSEDS3_REGION" Require-Secret $values "DECOMPOSEDS3_ACCESS_KEY" Require-Secret $values "DECOMPOSEDS3_SECRET_KEY" Require-Value $values "DECOMPOSEDS3_BUCKET" } if (Test-Enabled $values "DECOMPOSEDS3_MINIO") { Add-Error "DECOMPOSEDS3_MINIO is for test/lab installs and must not be enabled for public production." if (-not (Test-ComposeFileIncludes $composeFile "DECOMPOSEDS3_MINIO" "minio.yml")) { Add-Error "COMPOSE_FILE must include minio.yml or the `${DECOMPOSEDS3_MINIO:-} expansion when DECOMPOSEDS3_MINIO is enabled." } } if (Test-Enabled $values "KEYCLOAK") { Require-ExactValue $values "KEYCLOAK" ":keycloak.yml" if (-not (Test-ComposeFileIncludes $composeFile "KEYCLOAK" "keycloak.yml")) { Add-Error "COMPOSE_FILE must include keycloak.yml or the `${KEYCLOAK:-} expansion when KEYCLOAK is enabled." } Require-Value $values "KEYCLOAK_DOMAIN" Require-Value $values "KEYCLOAK_REALM" Require-Secret $values "KEYCLOAK_POSTGRES_PASSWORD" Require-Value $values "KEYCLOAK_ADMIN_USER" Require-Secret $values "KEYCLOAK_ADMIN_PASSWORD" } if ([string]::IsNullOrWhiteSpace((Get-EnvValue $values "OC_CONFIG_DIR")) -or [string]::IsNullOrWhiteSpace((Get-EnvValue $values "OC_DATA_DIR"))) { Add-WarningMessage "OC_CONFIG_DIR and OC_DATA_DIR are not both set; docker volumes are harder to back up and restore." } if ($errors.Count -gt 0) { throw "Production env validation failed: $($errors.Count) error(s), $($warnings.Count) warning(s)." } if (-not $AllowWarnings -and $warnings.Count -gt 0) { throw "Production env validation passed with $($warnings.Count) warning(s). Re-run with -AllowWarnings if this is expected." } Write-Output "Production env validation passed: $($warnings.Count) warning(s)."