diff --git a/fluxer_docs/scripts/VerifyDocsCoverage.ts b/fluxer_docs/scripts/VerifyDocsCoverage.ts index e74e9e285..24087a652 100644 --- a/fluxer_docs/scripts/VerifyDocsCoverage.ts +++ b/fluxer_docs/scripts/VerifyDocsCoverage.ts @@ -1314,6 +1314,33 @@ console.log('self-hosting guide against deploy/self-hosting'); } } + const COMPOSE_DEFAULT_NAMES = ['compose.yaml', 'compose.yml', 'docker-compose.yml', 'docker-compose.yaml']; + const shellComposeNames = shellRows('fluxer_compose_names', 'NAMES', 'the install.sh compose name list'); + const powershellComposeNames: Array = []; + for (const row of powershellRows('FluxerComposeNames', 'the install.ps1 compose name list')) { + const parsed = row.match(/^\s*'([^']+)'\s*$/u); + if (parsed == null) { + problems.push(`the install.ps1 compose name list carries \`${row.trim()}\`, which is not a quoted file name`); + continue; + } + powershellComposeNames.push(parsed[1]); + } + for (const [script, list] of [ + ['install.sh', shellComposeNames], + ['install.ps1', powershellComposeNames], + ] as const) { + if (list.join(', ') !== COMPOSE_DEFAULT_NAMES.join(', ')) { + problems.push( + `${script} resolves compose files as [${list.join(', ')}] and Compose resolves them as [${COMPOSE_DEFAULT_NAMES.join(', ')}]`, + ); + } + } + if (!shellStackFiles.includes('docker-compose.yml') || !COMPOSE_DEFAULT_NAMES.includes('docker-compose.yml')) { + problems.push( + 'the installers no longer download docker-compose.yml, so the name they map onto an instance is unclear', + ); + } + const PIPE_TO_SHELL = /(?:curl|wget|iwr|Invoke-WebRequest)[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|iex|Invoke-Expression)\b/iu; const docsPages = await walk(DOCS_ROOT); @@ -1419,10 +1446,15 @@ console.log('self-hosting guide against deploy/self-hosting'); await writeFile(path.join(instance, '.env'), 'FLUXER_DOMAIN=x.example\nFLUXER_IMAGE_TAG=2026.813.205040\n'); await writeFile(path.join(instance, 'docker-compose.yml'), 'name: fluxer\nservices:\n api:\n image: stub\n'); - const plannedRef = (label: string, args: ReadonlyArray): string | null => { + const planned = (label: string, args: ReadonlyArray, cwd?: string): string | null => { const run = spawnSync('sh', [path.join(INSTALLER_ROOT, 'install.sh'), ...args], { + cwd, encoding: 'utf8', - env: {...process.env, PATH: `${stubBin}${path.delimiter}${process.env.PATH ?? ''}`}, + env: { + ...process.env, + PATH: `${stubBin}${path.delimiter}${process.env.PATH ?? ''}`, + ...(cwd == null ? {} : {PWD: cwd}), + }, }); if (run.error != null) { problems.push(`install.sh ${label} could not run: ${run.error.message}`); @@ -1432,7 +1464,15 @@ console.log('self-hosting guide against deploy/self-hosting'); problems.push(`install.sh ${label} exited ${String(run.status)}: ${run.stderr.trim()}`); return null; } - const line = run.stdout.match(/^ {2}ref\s+(\S+)$/mu); + return run.stdout; + }; + + const plannedRef = (label: string, args: ReadonlyArray): string | null => { + const stdout = planned(label, args); + if (stdout == null) { + return null; + } + const line = stdout.match(/^ {2}ref\s+(\S+)$/mu); if (line == null) { problems.push(`install.sh ${label} printed no ref line`); return null; @@ -1472,6 +1512,38 @@ console.log('self-hosting guide against deploy/self-hosting'); problems.push(`install.sh ${label} plans ref ${resolved}, and the image tag it pairs with wants ${expected}`); } } + + const composeYmlInstance = path.join(sandbox, 'compose-yml-instance'); + await mkdir(composeYmlInstance, {recursive: true}); + await writeFile( + path.join(composeYmlInstance, '.env'), + 'FLUXER_DOMAIN=x.example\nFLUXER_IMAGE_TAG=2026.813.205040\n', + ); + await writeFile(path.join(composeYmlInstance, 'compose.yml'), 'name: fluxer\nservices:\n api:\n image: stub\n'); + const COMPOSE_NAME_CASES: ReadonlyArray, string | undefined]> = [ + [ + 'under --update against a compose.yml instance', + ['--update', '--dry-run', '--allow-root', '--dir', composeYmlInstance], + undefined, + ], + [ + 'under --update standing in a compose.yml instance', + ['--update', '--dry-run', '--allow-root'], + composeYmlInstance, + ], + ]; + for (const [label, args, cwd] of COMPOSE_NAME_CASES) { + const stdout = planned(label, args, cwd); + if (stdout == null) { + continue; + } + if (!/^ {4}compose\.yml is unchanged$/mu.test(stdout)) { + problems.push(`install.sh ${label} does not plan the refreshed stack file onto compose.yml`); + } + if (/^ {4}docker-compose\.yml is new$/mu.test(stdout)) { + problems.push(`install.sh ${label} plans a docker-compose.yml that Compose would never load there`); + } + } } finally { await rm(sandbox, {recursive: true, force: true}); } diff --git a/fluxer_docs/src/installer/install.ps1 b/fluxer_docs/src/installer/install.ps1 index 6a6ee864e..e623e6e3d 100644 --- a/fluxer_docs/src/installer/install.ps1 +++ b/fluxer_docs/src/installer/install.ps1 @@ -108,6 +108,32 @@ $FluxerStackFiles = @( '.env.example' ) +$FluxerComposeNames = @( + 'compose.yaml' + 'compose.yml' + 'docker-compose.yml' + 'docker-compose.yaml' +) + +$script:FluxerComposeBase = 'docker-compose.yml' +$script:FluxerComposeBaseFrom = '' + +function Get-FluxerComposeNameIn([string]$Directory) { + foreach ($name in $FluxerComposeNames) { + if (Test-Path -LiteralPath (Join-Path $Directory $name)) { + return $name + } + } + return '' +} + +function Get-FluxerPlacedName([string]$Name) { + if ($Name -eq 'docker-compose.yml') { + return $script:FluxerComposeBase + } + return $Name +} + # The file Compose bind-mounts from the working directory, with the service that mounts it. # Compose decides whether to recreate a container by comparing its configuration, and the # contents of a bind-mounted file are not part of that comparison, so a changed Caddyfile survives @@ -641,7 +667,7 @@ function Get-FluxerStackFiles([string]$StagingDir, [string]$RefValue) { # for an edit. function Move-FluxerStackFiles([string]$StagingDir, [string]$TargetDir) { foreach ($name in $FluxerStackFiles) { - Move-Item -LiteralPath (Join-Path $StagingDir $name) -Destination (Join-Path $TargetDir $name) -Force + Move-Item -LiteralPath (Join-Path $StagingDir $name) -Destination (Join-Path $TargetDir (Get-FluxerPlacedName $name)) -Force } } @@ -840,7 +866,7 @@ function Get-FluxerComposeProject([string]$TargetDir) { if ($null -ne $env:COMPOSE_PROJECT_NAME -and $env:COMPOSE_PROJECT_NAME.Length -gt 0) { return $env:COMPOSE_PROJECT_NAME } - foreach ($line in [System.IO.File]::ReadAllText((Join-Path $TargetDir 'docker-compose.yml')).Split("`n")) { + foreach ($line in [System.IO.File]::ReadAllText((Join-Path $TargetDir $script:FluxerComposeBase)).Split("`n")) { if ($line -match '^name:\s*(\S+)') { return $Matches[1] } @@ -1046,7 +1072,7 @@ function Restart-FluxerMounts($Entries, [string]$TargetDir) { $services = @(Get-FluxerComposeServices) foreach ($entry in $Entries) { if ($services -notcontains $entry.Service) { - Write-FluxerLine "Skipping the restart of $($entry.Service), because the docker-compose.yml in $TargetDir defines no service by that name." + Write-FluxerLine "Skipping the restart of $($entry.Service), because the $($script:FluxerComposeBase) in $TargetDir defines no service by that name." continue } Write-FluxerLine "Restarting $($entry.Service), because $($entry.Name) is mounted into it and up -d does not reload a mounted file." @@ -1225,7 +1251,7 @@ function Save-FluxerCurrentFiles([string]$Record, [string]$TargetDir, [string]$E Copy-Item -LiteralPath $EnvPath -Destination $envCopy -Force Set-FluxerPrivateFile $envCopy foreach ($name in $FluxerStackFiles) { - $source = Join-Path $TargetDir $name + $source = Join-Path $TargetDir (Get-FluxerPlacedName $name) $length = Get-FluxerFileLength $source if ($length -gt 0) { Copy-Item -LiteralPath $source -Destination (Join-Path $Record $name) -Force @@ -1427,7 +1453,7 @@ function Backup-FluxerInstance([string]$Record, [string]$TargetDir, [string]$Pro # The refreshed file is still staged when this runs, so a refusal here leaves the instance exactly # as it was. function Assert-FluxerPostgresMajor([string]$TargetDir, [string]$StagingDir) { - $old = Get-FluxerPostgresMajor (Join-Path $TargetDir 'docker-compose.yml') + $old = Get-FluxerPostgresMajor (Join-Path $TargetDir $script:FluxerComposeBase) $new = Get-FluxerPostgresMajor (Join-Path $StagingDir 'docker-compose.yml') if ($old.Length -eq 0 -or $new.Length -eq 0 -or $old -eq $new) { return @@ -1515,21 +1541,22 @@ function Show-FluxerUpdatePlan([string]$TargetDir, [string]$EnvPath, [string]$Ba Write-FluxerLine ' File changes:' $changed = 0 foreach ($name in $FluxerStackFiles) { - $current = Join-Path $TargetDir $name + $placed = Get-FluxerPlacedName $name + $current = Join-Path $TargetDir $placed if (-not (Test-Path -LiteralPath $current)) { - Write-FluxerLine " $name is new" + Write-FluxerLine " $placed is new" $changed++ } elseif (Test-FluxerSameFile $current (Join-Path $staging $name)) { - Write-FluxerLine " $name is unchanged" + Write-FluxerLine " $placed is unchanged" } else { - Write-FluxerLine " $name changes" + Write-FluxerLine " $placed changes" $changed++ } } if ($changed -eq 0) { Write-FluxerLine " Note: ref $Ref moves no stack file" } - $old = Get-FluxerPostgresMajor (Join-Path $TargetDir 'docker-compose.yml') + $old = Get-FluxerPostgresMajor (Join-Path $TargetDir $script:FluxerComposeBase) $new = Get-FluxerPostgresMajor (Join-Path $staging 'docker-compose.yml') if ($old.Length -gt 0 -and $new.Length -gt 0 -and $old -ne $new) { Write-FluxerLine " Refusal: postgres moves from $old to $new, which this script does not do" @@ -1706,7 +1733,7 @@ function Invoke-FluxerRollback([string]$TargetDir, [string]$EnvPath, [string]$Ba $source = Join-Path $record $name $length = Get-FluxerFileLength $source if ($length -gt 0) { - Copy-Item -LiteralPath $source -Destination (Join-Path $TargetDir $name) -Force + Copy-Item -LiteralPath $source -Destination (Join-Path $TargetDir (Get-FluxerPlacedName $name)) -Force } elseif (Test-Path -LiteralPath $source) { Stop-Fluxer "$source is empty, so restoring it would replace a working file with nothing. Nothing was restored. Take the file from another record or from the ref the record names." $FluxerExitRefused } @@ -1739,13 +1766,20 @@ function Assert-FluxerInstance([string]$TargetDir, [string]$EnvPath) { if (-not (Test-Path -LiteralPath $EnvPath)) { Stop-Fluxer "No .env in $TargetDir. That directory holds no instance. Run install.ps1 with neither -Update nor -Rollback to set one up." $FluxerExitPrerequisite } - $compose = Join-Path $TargetDir 'docker-compose.yml' + Resolve-FluxerComposeBase $TargetDir $EnvPath + $compose = Join-Path $TargetDir $script:FluxerComposeBase if (-not (Test-Path -LiteralPath $compose)) { - Stop-Fluxer "No docker-compose.yml in $TargetDir. That directory does not hold an instance." $FluxerExitPrerequisite + if ($script:FluxerComposeBaseFrom.Length -gt 0) { + Stop-Fluxer "$($script:FluxerComposeBaseFrom) names $($script:FluxerComposeBase) first, and $compose is not there. Put that file back, or name the file the instance runs on first in COMPOSE_FILE." $FluxerExitPrerequisite + } + Stop-Fluxer "No compose file in $TargetDir. Compose looks for compose.yaml, compose.yml, docker-compose.yml and docker-compose.yaml there, and that directory holds none of them, so it does not hold an instance." $FluxerExitPrerequisite } if ((Get-FluxerFileLength $compose) -eq 0) { Stop-Fluxer "$compose is empty. A redirect that captured a failed download leaves that, and Compose refuses an empty compose file. Put the file back from a backup or from the record of the last upgrade, then run this again." $FluxerExitPrerequisite } + if ($script:FluxerComposeBase -ne 'docker-compose.yml') { + Write-FluxerLine "Compose loads $($script:FluxerComposeBase) in $TargetDir, so the stack's docker-compose.yml is written to that name." + } } # Compose reads COMPOSE_FILE from .env and loads every file it names before it answers anything, so @@ -1776,7 +1810,7 @@ function Get-FluxerEnvScalar([string]$EnvPath, [string]$Name) { return $raw } -function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) { +function Get-FluxerComposeSetting([string]$EnvPath) { $value = '' $source = 'the environment' if ($null -ne $env:COMPOSE_FILE) { @@ -1786,9 +1820,6 @@ function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) { $value = Get-FluxerEnvScalar $EnvPath 'COMPOSE_FILE' $source = $EnvPath } - if ($value.Length -eq 0) { - return - } $separator = '' if ($null -ne $env:COMPOSE_PATH_SEPARATOR) { $separator = [string]$env:COMPOSE_PATH_SEPARATOR @@ -1799,6 +1830,50 @@ function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) { if ($separator.Length -eq 0) { $separator = [System.IO.Path]::PathSeparator } + return [pscustomobject]@{Value = $value; Source = $source; Separator = $separator} +} + +function Resolve-FluxerComposeBase([string]$TargetDir, [string]$EnvPath) { + $setting = Get-FluxerComposeSetting $EnvPath + $script:FluxerComposeBaseFrom = '' + foreach ($name in $setting.Value.Split([string[]]$setting.Separator, [System.StringSplitOptions]::None)) { + if ($name.Length -eq 0) { + continue + } + $base = $name + foreach ($prefix in @('./', '.\')) { + if ($base.StartsWith($prefix, [System.StringComparison]::Ordinal)) { + $base = $base.Substring($prefix.Length) + } + } + foreach ($root in @("$TargetDir/", "$TargetDir\")) { + if ($base.StartsWith($root, [System.StringComparison]::OrdinalIgnoreCase)) { + $base = $base.Substring($root.Length) + } + } + $script:FluxerComposeBaseFrom = "COMPOSE_FILE from $($setting.Source)" + if ($base.Contains('/') -or $base.Contains('\')) { + Stop-Fluxer "$($script:FluxerComposeBaseFrom) names $name first, and that file is not directly in $TargetDir. This script refreshes only the files in the directory it acts on, so the upgrade would leave the file Compose loads on the old stack. Move it into $TargetDir and name it there, or upgrade by hand." $FluxerExitPrerequisite + } + $script:FluxerComposeBase = $base + return + } + $found = Get-FluxerComposeNameIn $TargetDir + if ($found.Length -gt 0) { + $script:FluxerComposeBase = $found + } else { + $script:FluxerComposeBase = 'docker-compose.yml' + } +} + +function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) { + $setting = Get-FluxerComposeSetting $EnvPath + $value = $setting.Value + $source = $setting.Source + $separator = $setting.Separator + if ($value.Length -eq 0) { + return + } foreach ($name in $value.Split([string[]]$separator, [System.StringSplitOptions]::None)) { if ($name.Length -eq 0) { continue @@ -1855,15 +1930,18 @@ function Invoke-FluxerInstall { $targetPath = $Dir $adoptedCwd = $false + $fellBack = $false if ($targetPath.Length -eq 0) { $here = (Get-Location).Path $hereEnv = Join-Path $here '.env' $hereIsFluxer = (Test-Path -LiteralPath $hereEnv) -and (@(Get-FluxerEnvLines $hereEnv | Where-Object {$_.StartsWith('FLUXER_')}).Count -gt 0) - if (($Update -or $Rollback) -and (Get-FluxerFileLength (Join-Path $here 'docker-compose.yml')) -gt 0 -and $hereIsFluxer) { + $hereCompose = Get-FluxerComposeNameIn $here + if (($Update -or $Rollback) -and $hereCompose.Length -gt 0 -and (Get-FluxerFileLength (Join-Path $here $hereCompose)) -gt 0 -and $hereIsFluxer) { $targetPath = $here $adoptedCwd = $true } else { $targetPath = Join-Path $HOME 'fluxer' + $fellBack = $Update -or $Rollback } } $targetDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($targetPath) @@ -1876,6 +1954,9 @@ function Invoke-FluxerInstall { if ($adoptedCwd) { Write-FluxerLine "Acting on the instance in $targetDir, the working directory. Pass -Dir to name another." } + if ($fellBack) { + Write-FluxerLine "The working directory holds no instance, so this acts on $targetDir. Pass -Dir to name another." + } if ($Update -or $Rollback) { Assert-FluxerInstance $targetDir $envPath @@ -1886,7 +1967,7 @@ function Invoke-FluxerInstall { Assert-FluxerComposeFiles $targetDir $envPath $project = Get-FluxerComposeProject $targetDir if ($project.Length -eq 0) { - Stop-Fluxer "docker-compose.yml in $targetDir declares no project name, so the volume names cannot be derived." $FluxerExitPrerequisite + Stop-Fluxer "$($script:FluxerComposeBase) in $targetDir declares no project name, so the volume names cannot be derived." $FluxerExitPrerequisite } Push-Location -LiteralPath $targetDir try { diff --git a/fluxer_docs/src/installer/install.sh b/fluxer_docs/src/installer/install.sh index c43f0e478..b4b47b548 100644 --- a/fluxer_docs/src/installer/install.sh +++ b/fluxer_docs/src/installer/install.sh @@ -131,6 +131,36 @@ Caddyfile FILES } +fluxer_compose_names() { + cat <<'NAMES' +compose.yaml +compose.yml +docker-compose.yml +docker-compose.yaml +NAMES +} + +fluxer_compose_name_in() { + for fluxer_candidate in $(fluxer_compose_names); do + if [ -e "$1/$fluxer_candidate" ]; then + printf '%s' "$fluxer_candidate" + return 0 + fi + done + return 1 +} + +fluxer_compose_base='docker-compose.yml' +fluxer_compose_base_from='' + +fluxer_placed_name() { + if [ "$1" = 'docker-compose.yml' ]; then + printf '%s' "$fluxer_compose_base" + else + printf '%s' "$1" + fi +} + # The file Compose bind-mounts from the working directory, with the service that # mounts it. # @@ -372,15 +402,21 @@ done # # An install writes a new instance, so it never adopts the working directory. if [ -z "$opt_dir" ]; then + fluxer_here_compose=$(fluxer_compose_name_in "$(pwd)" || true) if { [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; } && - [ -s "$(pwd)/docker-compose.yml" ] && [ -e "$(pwd)/.env" ] && - grep -q '^FLUXER_' "$(pwd)/.env" 2>/dev/null; then + [ -n "$fluxer_here_compose" ] && [ -s "$(pwd)/$fluxer_here_compose" ] && + [ -e "$(pwd)/.env" ] && grep -q '^FLUXER_' "$(pwd)/.env" 2>/dev/null; then opt_dir="$(pwd)" fluxer_say "Acting on the instance in $opt_dir, the working directory. Pass --dir to name another." - elif [ -n "${HOME:-}" ]; then - opt_dir="$HOME/fluxer" else - opt_dir="$(pwd)/fluxer" + if [ -n "${HOME:-}" ]; then + opt_dir="$HOME/fluxer" + else + opt_dir="$(pwd)/fluxer" + fi + if [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; then + fluxer_say "The working directory holds no instance, so this acts on $opt_dir. Pass --dir to name another." + fi fi fi case $opt_dir in @@ -749,7 +785,7 @@ fluxer_fetch_stack() { fluxer_place_stack() { while read -r fluxer_file; do [ -n "$fluxer_file" ] || continue - mv "$fluxer_scratch/$fluxer_file.part" "$opt_dir/$fluxer_file" + mv "$fluxer_scratch/$fluxer_file.part" "$opt_dir/$(fluxer_placed_name "$fluxer_file")" done < "$fluxer_scratch/files" fluxer_say "Stack files in $opt_dir are at ref $opt_ref." } @@ -759,7 +795,7 @@ fluxer_compose_project() { printf '%s' "$COMPOSE_PROJECT_NAME" return 0 fi - sed -n 's/^name: *//p' "$opt_dir/docker-compose.yml" | head -n 1 + sed -n 's/^name: *//p' "$opt_dir/$fluxer_compose_base" | head -n 1 } fluxer_project='' @@ -767,7 +803,7 @@ fluxer_project='' fluxer_set_project() { fluxer_project=$(fluxer_compose_project) if [ -z "$fluxer_project" ]; then - fluxer_fail 2 "docker-compose.yml in $opt_dir declares no project name, so the volume names cannot be derived." + fluxer_fail 2 "$fluxer_compose_base in $opt_dir declares no project name, so the volume names cannot be derived." fi } @@ -1104,11 +1140,18 @@ fluxer_require_instance() { if [ ! -e "$opt_dir/.env" ]; then fluxer_fail 2 "No .env in $opt_dir. That directory holds no instance. Run install.sh with neither --update nor --rollback to set one up." fi - if [ ! -e "$opt_dir/docker-compose.yml" ]; then - fluxer_fail 2 "No docker-compose.yml in $opt_dir. That directory does not hold an instance." + fluxer_resolve_compose_base + if [ ! -e "$opt_dir/$fluxer_compose_base" ]; then + if [ -n "$fluxer_compose_base_from" ]; then + fluxer_fail 2 "$fluxer_compose_base_from names $fluxer_compose_base first, and $opt_dir/$fluxer_compose_base is not there. Put that file back, or name the file the instance runs on first in COMPOSE_FILE." + fi + fluxer_fail 2 "No compose file in $opt_dir. Compose looks for compose.yaml, compose.yml, docker-compose.yml and docker-compose.yaml there, and that directory holds none of them, so it does not hold an instance." fi - if [ ! -s "$opt_dir/docker-compose.yml" ]; then - fluxer_fail 2 "$opt_dir/docker-compose.yml is empty. A redirect that captured a failed download leaves that, and Compose refuses an empty compose file. Put the file back from a backup or from the record of the last upgrade, then run this again." + if [ ! -s "$opt_dir/$fluxer_compose_base" ]; then + fluxer_fail 2 "$opt_dir/$fluxer_compose_base is empty. A redirect that captured a failed download leaves that, and Compose refuses an empty compose file. Put the file back from a backup or from the record of the last upgrade, then run this again." + fi + if [ "$fluxer_compose_base" != 'docker-compose.yml' ]; then + fluxer_say "Compose loads $fluxer_compose_base in $opt_dir, so the stack's docker-compose.yml is written to that name." fi } @@ -1141,14 +1184,13 @@ fluxer_env_scalar() { printf '%s' "$fluxer_scalar" } -fluxer_require_compose_files() { +fluxer_read_compose_setting() { fluxer_compose_file=${COMPOSE_FILE:-} fluxer_compose_from="the environment" if [ -z "$fluxer_compose_file" ]; then fluxer_compose_file=$(fluxer_env_scalar COMPOSE_FILE) fluxer_compose_from="$opt_dir/.env" fi - [ -n "$fluxer_compose_file" ] || return 0 fluxer_path_sep=${COMPOSE_PATH_SEPARATOR:-} if [ -z "$fluxer_path_sep" ]; then fluxer_path_sep=$(fluxer_env_scalar COMPOSE_PATH_SEPARATOR) @@ -1156,6 +1198,36 @@ fluxer_require_compose_files() { if [ -z "$fluxer_path_sep" ]; then fluxer_path_sep=':' fi +} + +fluxer_resolve_compose_base() { + fluxer_read_compose_setting + fluxer_compose_base_from='' + fluxer_rest=$fluxer_compose_file + while [ -n "$fluxer_rest" ]; do + fluxer_name=${fluxer_rest%%"$fluxer_path_sep"*} + case $fluxer_rest in + *"$fluxer_path_sep"*) fluxer_rest=${fluxer_rest#*"$fluxer_path_sep"} ;; + *) fluxer_rest='' ;; + esac + [ -n "$fluxer_name" ] || continue + fluxer_compose_base=${fluxer_name#./} + fluxer_compose_base=${fluxer_compose_base#"$opt_dir"/} + fluxer_compose_base_from="COMPOSE_FILE from $fluxer_compose_from" + break + done + if [ -z "$fluxer_compose_base_from" ]; then + fluxer_compose_base=$(fluxer_compose_name_in "$opt_dir" || printf '%s' 'docker-compose.yml') + return 0 + fi + case $fluxer_compose_base in + */*) fluxer_fail 2 "$fluxer_compose_base_from names $fluxer_name first, and that file is not directly in $opt_dir. This script refreshes only the files in the directory it acts on, so the upgrade would leave the file Compose loads on the old stack. Move it into $opt_dir and name it there, or upgrade by hand." ;; + esac +} + +fluxer_require_compose_files() { + fluxer_read_compose_setting + [ -n "$fluxer_compose_file" ] || return 0 fluxer_compose_count=0 fluxer_rest=$fluxer_compose_file while [ -n "$fluxer_rest" ]; do @@ -1185,7 +1257,7 @@ Leave the COMPOSE_FILE line as it is. Without $fluxer_name the edge container bi done if [ "$fluxer_compose_count" -gt 1 ] && ! fluxer_version_ge "$fluxer_compose_version" "$FLUXER_MIN_COMPOSE_OVERLAY"; then - fluxer_fail 2 "COMPOSE_FILE from $fluxer_compose_from loads $fluxer_compose_count files and this host runs Compose $fluxer_compose_version. Every overlay this script downloads uses the !override tag, which needs Compose $FLUXER_MIN_COMPOSE_OVERLAY or newer. Upgrade Compose, or load only docker-compose.yml." + fluxer_fail 2 "COMPOSE_FILE from $fluxer_compose_from loads $fluxer_compose_count files and this host runs Compose $fluxer_compose_version. Every overlay this script downloads uses the !override tag, which needs Compose $FLUXER_MIN_COMPOSE_OVERLAY or newer. Upgrade Compose, or load only $fluxer_compose_base." fi } @@ -1315,10 +1387,11 @@ fluxer_save_current_files() { chmod 600 "$fluxer_record/.env" while read -r fluxer_file; do [ -n "$fluxer_file" ] || continue - if [ -s "$opt_dir/$fluxer_file" ]; then - cp -p "$opt_dir/$fluxer_file" "$fluxer_record/$fluxer_file" - elif [ -e "$opt_dir/$fluxer_file" ]; then - fluxer_fail 7 "$opt_dir/$fluxer_file is empty, so the record would hold a file a rollback could not use. Put the file back before upgrading." + fluxer_placed="$opt_dir/$(fluxer_placed_name "$fluxer_file")" + if [ -s "$fluxer_placed" ]; then + cp -p "$fluxer_placed" "$fluxer_record/$fluxer_file" + elif [ -e "$fluxer_placed" ]; then + fluxer_fail 7 "$fluxer_placed is empty, so the record would hold a file a rollback could not use. Put the file back before upgrading." fi done < "$fluxer_scratch/files" } @@ -1499,7 +1572,7 @@ fluxer_postgres_major() { # The refreshed file is still in the scratch directory when this runs, so a # refusal here leaves the instance exactly as it was. fluxer_guard_postgres_major() { - fluxer_old_major=$(fluxer_postgres_major "$opt_dir/docker-compose.yml") + fluxer_old_major=$(fluxer_postgres_major "$opt_dir/$fluxer_compose_base") fluxer_new_major=$(fluxer_postgres_major "$fluxer_scratch/docker-compose.yml.part") if [ -z "$fluxer_old_major" ] || [ -z "$fluxer_new_major" ]; then return 0 @@ -1550,7 +1623,7 @@ $(fluxer_compose_error ' ')" while read -r fluxer_file fluxer_service; do [ -n "$fluxer_service" ] || continue if ! grep -qxF "$fluxer_service" "$fluxer_scratch/services"; then - fluxer_say "Skipping the restart of $fluxer_service, because the docker-compose.yml in $opt_dir defines no service by that name." + fluxer_say "Skipping the restart of $fluxer_service, because the $fluxer_compose_base in $opt_dir defines no service by that name." continue fi fluxer_say "Restarting $fluxer_service, because $fluxer_file is mounted into it and up -d does not reload a mounted file." @@ -1656,20 +1729,21 @@ fluxer_plan_update() { fluxer_changed=0 while read -r fluxer_file; do [ -n "$fluxer_file" ] || continue - if [ ! -e "$opt_dir/$fluxer_file" ]; then - fluxer_say " $fluxer_file is new" + fluxer_placed=$(fluxer_placed_name "$fluxer_file") + if [ ! -e "$opt_dir/$fluxer_placed" ]; then + fluxer_say " $fluxer_placed is new" fluxer_changed=1 - elif cmp -s "$opt_dir/$fluxer_file" "$fluxer_scratch/$fluxer_file.part"; then - fluxer_say " $fluxer_file is unchanged" + elif cmp -s "$opt_dir/$fluxer_placed" "$fluxer_scratch/$fluxer_file.part"; then + fluxer_say " $fluxer_placed is unchanged" else - fluxer_say " $fluxer_file changes" + fluxer_say " $fluxer_placed changes" fluxer_changed=1 fi done < "$fluxer_scratch/files" if [ "$fluxer_changed" -eq 0 ]; then fluxer_say " note ref $opt_ref moves no stack file" fi - fluxer_old_major=$(fluxer_postgres_major "$opt_dir/docker-compose.yml") + fluxer_old_major=$(fluxer_postgres_major "$opt_dir/$fluxer_compose_base") fluxer_new_major=$(fluxer_postgres_major "$fluxer_scratch/docker-compose.yml.part") if [ -n "$fluxer_old_major" ] && [ -n "$fluxer_new_major" ] && [ "$fluxer_old_major" != "$fluxer_new_major" ]; then fluxer_say " refusal postgres moves from $fluxer_old_major to $fluxer_new_major, which this script does not do" @@ -1862,7 +1936,7 @@ fluxer_run_rollback() { while read -r fluxer_file; do [ -n "$fluxer_file" ] || continue if [ -s "$fluxer_rollback_dir/$fluxer_file" ]; then - cp -p "$fluxer_rollback_dir/$fluxer_file" "$opt_dir/$fluxer_file" + cp -p "$fluxer_rollback_dir/$fluxer_file" "$opt_dir/$(fluxer_placed_name "$fluxer_file")" elif [ -e "$fluxer_rollback_dir/$fluxer_file" ]; then fluxer_fail 3 "$fluxer_rollback_dir/$fluxer_file is empty, so restoring it would replace a working file with nothing. Nothing was restored. Take the file from another record or from the ref the record names." fi