Compare commits

..

2 Commits

Author SHA1 Message Date
Lance Pioch
d7b6914170 docker: strip quotes and carriage returns when loading .env vars
A .env edited on Windows leaves a trailing \r in exported values, which
breaks the APP_INSTALLED comparison and the DB_HOST wait, and single
quoted values kept their quotes. Strip both along with double quotes.
2026-07-18 10:25:53 -04:00
Lance Pioch
717370ed48 docker: harden entrypoint .env parsing and key generation
The .env loader used unanchored greps, so a comment containing a variable
name, or another variable that merely contains the name (e.g.
SOME_APP_KEY_BACKUP), matched too. With multiple matching lines the
export received a multiline string, failed, and 'ash -e' killed the
container at boot.

- match only real assignments anchored at the start of a line
- let container environment variables take precedence over .env for the
  entrypoint's own vars, matching Laravel's precedence and the comment's
  stated intent
- generate APP_KEY in the standard base64: format (32 random bytes)
  instead of a hand-rolled alphanumeric string
- add a SKIP_MIGRATIONS escape hatch for multi-replica deployments where
  automatic 'migrate --force' on every boot would race
2026-07-15 20:39:12 -04:00
2 changed files with 84 additions and 97 deletions

View File

@@ -13,92 +13,57 @@ return new class extends Migration
*/
public function up(): void
{
if (!Schema::hasTable('backup_hosts')) {
Schema::create('backup_hosts', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('schema');
$table->json('configuration')->nullable();
$table->timestamps();
});
}
if (!Schema::hasTable('backup_host_node')) {
Schema::create('backup_host_node', function (Blueprint $table) {
$table->unsignedInteger('node_id');
$table->foreign('node_id')->references('id')->on('nodes')->cascadeOnDelete();
$table->unsignedInteger('backup_host_id');
$table->foreign('backup_host_id')->references('id')->on('backup_hosts')->cascadeOnDelete();
$table->timestamps();
$table->unique(['node_id']);
});
}
$backupHost = BackupHost::first();
if (!$backupHost) {
$oldDriver = env('APP_BACKUP_DRIVER', 'wings');
$oldConfiguration = null;
if ($oldDriver === 's3') {
$oldConfiguration = [
'region' => env('AWS_DEFAULT_REGION'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'bucket' => env('AWS_BACKUPS_BUCKET'),
'prefix' => env('AWS_BACKUPS_BUCKET', ''),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false),
'storage_class' => env('AWS_BACKUPS_STORAGE_CLASS'),
];
}
$backupHost = BackupHost::create([
'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
'schema' => $oldDriver,
'configuration' => $oldConfiguration,
]);
}
// The column must start out nullable: adding it NOT NULL would give existing
// rows a value of 0, which the foreign key below would reject.
if (!Schema::hasColumn('backups', 'backup_host_id')) {
$hasDiskColumn = Schema::hasColumn('backups', 'disk');
Schema::table('backups', function (Blueprint $table) use ($hasDiskColumn) {
$column = $table->unsignedInteger('backup_host_id')->nullable();
// The disk column may already be gone on installs that were repaired by hand.
if ($hasDiskColumn) {
$column->after('disk');
}
});
}
DB::table('backups')
->whereNull('backup_host_id')
->orWhere('backup_host_id', 0)
->update(['backup_host_id' => $backupHost->id]);
Schema::table('backups', function (Blueprint $table) {
$table->unsignedInteger('backup_host_id')->nullable(false)->change();
Schema::create('backup_hosts', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('schema');
$table->json('configuration')->nullable();
$table->timestamps();
});
$foreignKeys = array_column(Schema::getForeignKeys('backups'), 'name');
if (!in_array('backups_backup_host_id_foreign', $foreignKeys)) {
Schema::table('backups', function (Blueprint $table) {
$table->foreign('backup_host_id')->references('id')->on('backup_hosts');
});
Schema::create('backup_host_node', function (Blueprint $table) {
$table->unsignedInteger('node_id');
$table->foreign('node_id')->references('id')->on('nodes')->cascadeOnDelete();
$table->unsignedInteger('backup_host_id');
$table->foreign('backup_host_id')->references('id')->on('backup_hosts')->cascadeOnDelete();
$table->timestamps();
$table->unique(['node_id']);
});
Schema::table('backups', function (Blueprint $table) {
$table->unsignedInteger('backup_host_id')->after('disk');
$table->foreign('backup_host_id')->references('id')->on('backup_hosts');
$table->dropColumn('disk');
});
$oldDriver = env('APP_BACKUP_DRIVER', 'wings');
$oldConfiguration = null;
if ($oldDriver === 's3') {
$oldConfiguration = [
'region' => env('AWS_DEFAULT_REGION'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'bucket' => env('AWS_BACKUPS_BUCKET'),
'prefix' => env('AWS_BACKUPS_BUCKET', ''),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'use_accelerate_endpoint' => env('AWS_BACKUPS_USE_ACCELERATE', false),
'storage_class' => env('AWS_BACKUPS_STORAGE_CLASS'),
];
}
if (Schema::hasColumn('backups', 'disk')) {
Schema::table('backups', function (Blueprint $table) {
$table->dropColumn('disk');
});
}
$backupHost = BackupHost::create([
'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
'schema' => $oldDriver,
'configuration' => $oldConfiguration,
]);
DB::table('backups')->update(['backup_host_id' => $backupHost->id]);
}
/**
@@ -113,8 +78,8 @@ return new class extends Migration
$table->dropColumn('backup_host_id');
});
Schema::dropIfExists('backup_host_node');
Schema::dropIfExists('backup_hosts');
Schema::dropIfExists('backup_host_node');
}
};

View File

@@ -4,19 +4,36 @@
# check for .env file or symlink and generate app keys if missing
if [ -f /pelican-data/.env ]; then
echo ".env vars exist."
# load specific env vars from .env used in the entrypoint and they are not already set
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT" "TRUSTED_PROXIES"; do
# load specific env vars from .env used in the entrypoint if they are not already set
for VAR in APP_KEY APP_INSTALLED DB_CONNECTION DB_HOST DB_PORT TRUSTED_PROXIES; do
echo "checking for ${VAR}"
## skip if it looks like it might try to execute code
if (grep "${VAR}" .env | grep -qE "\$\(|=\`|\$#"); then echo "var in .env may be executable or a comment, skipping"; continue; fi
# if the variable is in .env then set it
if (grep -q "${VAR}" .env); then
echo "loading ${VAR} from .env"
export "$(grep "${VAR}" .env | sed 's/"//g')"
# the container environment takes precedence, matching Laravel's own behavior
eval "CURRENT=\${${VAR}:-}"
if [ -n "${CURRENT}" ]; then
echo "${VAR} already set in environment, skipping"
continue
fi
## variable wasn't loaded or in the env to set
echo "didn't find variable to set"
# match only a real assignment at the start of a line, never comments or
# other variables that merely contain the name
if ! LINE=$(grep -m1 "^${VAR}=" .env); then
echo "didn't find variable to set"
continue
fi
## skip if it looks like it might try to execute code
case "$LINE" in
*'$('*|*'`'*)
echo "var in .env may be executable, skipping"
continue
;;
esac
echo "loading ${VAR} from .env"
# strip quotes and carriage returns so values from quoted or
# Windows-edited .env files export cleanly
export "$(echo "$LINE" | tr -d "\r\"'")"
done
else
echo ".env vars don't exist."
@@ -26,7 +43,7 @@ else
# manually generate a key because key generate --force fails
if [ -z "${APP_KEY}" ]; then
echo "No key set, Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
APP_KEY="base64:$(head -c 32 /dev/urandom | base64)"
echo "APP_KEY=$APP_KEY" > /pelican-data/.env
echo "Generated app key written to .env file"
else
@@ -57,8 +74,13 @@ if [ "${APP_INSTALLED}" = "true" ]; then
echo "using sqlite database"
fi
# run migration
php artisan migrate --force
# run migration, unless disabled (e.g. when running multiple replicas
# against the same database)
if [ "${SKIP_MIGRATIONS:-false}" = "true" ]; then
echo "Skipping migrations (SKIP_MIGRATIONS=true)"
else
php artisan migrate --force
fi
php artisan p:plugin:composer
fi