From ea08d916d94fbb451a55c76d1374cc015cd53605 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 24 Feb 2026 18:06:28 +1100 Subject: [PATCH 1/7] feat: migrate a scratch image --- Dockerfile | 88 +++++++++++----- scripts/generate-secrets | 118 ---------------------- shard.lock | 4 - shard.yml | 6 -- src/generate-secrets.cr | 193 ++++++++++++++++++++++++++++++++++++ src/migration.cr | 1 - src/tasks/backup.cr | 9 +- src/tasks/database.cr | 6 +- src/tasks/initialization.cr | 2 +- src/tasks/restore.cr | 4 +- src/utils/migrate_data.cr | 2 +- 11 files changed, 267 insertions(+), 166 deletions(-) delete mode 100755 scripts/generate-secrets create mode 100644 src/generate-secrets.cr diff --git a/Dockerfile b/Dockerfile index 8407870..6be3897 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,29 @@ ARG CRYSTAL_VERSION=latest FROM placeos/crystal:$CRYSTAL_VERSION AS build WORKDIR /app +# Install package updates since image release +RUN apk update && apk --no-cache --quiet upgrade + +# Update CA certificates +RUN update-ca-certificates + +# Install bash, postgresql-client +RUN apk add \ + --update \ + --no-cache \ + tzdata \ + 'apache2-utils>=2.4.52-r0' \ + expat \ + git \ + bash \ + jq \ + coreutils \ + 'libcurl>=7.79.1-r0' \ + openssh \ + openssl \ + wget \ + postgresql17-client + # Install shards for caching COPY shard.yml shard.yml COPY shard.override.yml shard.override.yml @@ -20,7 +43,6 @@ COPY src src RUN mkdir -p /app/bin # Build init -# TODO:: build static binaries, no libxml2-static available RUN shards build \ --error-trace \ --static \ @@ -29,11 +51,13 @@ RUN shards build \ --skip-postinstall RUN crystal build --static -o bin/task src/sam.cr +RUN crystal build --static -o bin/generate-secrets src/generate-secrets.cr SHELL ["/bin/ash", "-eo", "pipefail", "-c"] # Extract binary dependencies RUN mkdir deps -RUN for binary in /app/bin/*; do \ +RUN for binary in /app/bin/* /usr/bin/pg_dump /usr/bin/pg_restore /usr/bin/psql; do \ + [ -x "$binary" ] || continue; \ ldd "$binary" | \ tr -s '[:blank:]' '\n' | \ grep '^/' | \ @@ -42,36 +66,50 @@ RUN for binary in /app/bin/*; do \ RUN git clone https://github.com/PlaceOS/models -# Build a minimal docker image -FROM alpine:latest +# obtain busy box for file ops in scratch image +ARG TARGETARCH +RUN case "${TARGETARCH}" in \ + amd64) ARCH=x86_64 ;; \ + arm64) ARCH=armv8l ;; \ + *) echo "Unsupported arch: ${TARGETARCH}" && exit 1 ;; \ + esac && \ + wget -O /busybox https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-${ARCH} && \ + chmod +x /busybox + +# Create tmp directory with proper permissions +RUN rm -rf /tmp && mkdir -p /tmp && chmod 1777 /tmp +# Build a minimal docker image +FROM scratch WORKDIR /app +ENV PATH=$PATH:/:/app/bin -# Install package updates since image release -RUN apk update && apk --no-cache --quiet upgrade +# These are required for communicating with external services +COPY --from=build /etc/hosts /etc/hosts -# Install bash, postgresql-client -RUN apk add \ - --update \ - --no-cache \ - tzdata \ - 'apache2-utils>=2.4.52-r0' \ - expat \ - git \ - bash \ - jq \ - coreutils \ - 'libcurl>=7.79.1-r0' \ - openssh \ - openssl \ - postgresql17-client +# These provide certificate chain validation where communicating with external services over TLS +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /etc/gitconfig /etc/gitconfig +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt + +# This is required for Timezone support +COPY --from=build /usr/share/zoneinfo/ /usr/share/zoneinfo/ + +COPY --from=build /busybox /bin/busybox +SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] -# copy app -COPY scripts /app/scripts +# chmod for setting permissions on /tmp +COPY --from=build /tmp /tmp +RUN /bin/busybox chmod -R a+rwX /tmp +RUN /bin/busybox rm -rf /bin/busybox + +# Copy the app into place COPY --from=build /app/deps / COPY --from=build /app/bin /app/bin +COPY --from=build /usr/bin/pg_dump /pg_dump +COPY --from=build /usr/bin/pg_restore /pg_restore +COPY --from=build /usr/bin/psql /psql COPY --from=build /app/models/migration/db /app/db -ENV PATH="/app/bin:/app/scripts:${PATH}" - CMD ["/app/bin/start"] diff --git a/scripts/generate-secrets b/scripts/generate-secrets deleted file mode 100755 index 5915389..0000000 --- a/scripts/generate-secrets +++ /dev/null @@ -1,118 +0,0 @@ -#! /usr/bin/env bash -set -e - -# Colours -red='\033[0;31m' -green='\033[0;32m' -reset='\033[0m' - -influx_key_env=".env.influxdb" -chronograf_token_secret=".env.chronograf" -secret_key_env=".env.secret_key" -public_key_env=".env.public_key" -instance_telemetry_key_env=".env.instance_telemetry_key" -env=".env" - -set -a -if [[ -f "${influx_key_env}" ]]; then - . ${influx_key_env} -fi -if [[ -f "${chronograf_token_secret}" ]]; then - . ${chronograf_token_secret} -fi -if [[ -f "${secret_key_env}" ]]; then - . ${secret_key_env} -fi -if [[ -f "${public_key_env}" ]]; then - . ${public_key_env} -fi -if [[ -f "${instance_telemetry_key_env}" ]]; then - . ${instance_telemetry_key_env} -fi - -. ${env} -set +a - -if [[ -z "${TOKEN_SECRET}" ]]; then - TOKEN_SECRET=$(openssl rand -base64 256 | tr -d '\n') - - # Write the new secret to an env file - echo "TOKEN_SECRET=${TOKEN_SECRET}" > "$chronograf_token_secret" - echo "generated Chrongraf TOKEN_SECRET" -else - echo "already generated Chrongraf TOKEN_SECRET" -fi - -if [[ -z "${INFLUX_API_KEY}" || -z "${INFLUXDB_TOKEN}" ]]; then - INFLUX_API_KEY=$(openssl rand -base64 24) - # Write the API keys to an env file - echo "INFLUX_API_KEY=${INFLUX_API_KEY}" > "$influx_key_env" # for Source - echo "INFLUXDB_TOKEN=${INFLUX_API_KEY}" >> "$influx_key_env" # for Chronograf - echo "generated INFLUX_API_KEY, INFLUXDB_TOKEN" -else - echo "already generated INFLUX_API_KEY, INFLUXDB_TOKEN" -fi - -if [[ -z "${PLACE_SERVER_SECRET}" || -z "${JWT_PUBLIC}" || -z "${JWT_SECRET}" ]]; then - dir=$(mktemp -d) - - if [[ -n "${PLACE_SERVER_SECRET}" ]]; then - echo -e "${red}ERROR${reset}: the ${secret_key_env} file contains an existing secret." - echo "Please update the file should look like the following..." - echo "JWT_SECRET=" - echo "SECRET_KEY_BASE=" - echo "PLACE_SERVER_SECRET=" - echo "SERVER_SECRET=" - exit 1 - elif [[ -n "${JWT_SECRET}" && -z "${PLACE_SERVER_SECRET}" ]]; then - echo -e "${red}ERROR${reset}: this instance has previously been initialised with a default secret." - echo -e "See the ${green}server:rotate_server_secret${reset} task here https://github.com/PlaceOS/init#scripts" - echo "Please contact support@place.technology if you need help." - exit 1 - fi - - - ssh-keygen -t rsa -b 4096 -m PEM -f "${dir}/secret" -N "" &>/dev/null - openssl rsa -in "${dir}/secret" -pubout -outform PEM -out "${dir}/public" 0>&- &>/dev/null - - secret="$(base64 -w 0 -i "${dir}/secret")" - - # JWT secret - jwt_secret_env_key="JWT_SECRET=${secret}" - - # Used in Rails - secret_key_base_env_key="SECRET_KEY_BASE=${secret:0:30}" - - # Used in PlaceOS - place_server_secret_env_key="PLACE_SERVER_SECRET=${secret}" - server_secret_env_key="SERVER_SECRET=${secret}" - - echo ${jwt_secret_env_key} >$secret_key_env - echo ${secret_key_base_env_key} >>$secret_key_env - echo ${server_secret_env_key} >>$secret_key_env - echo ${place_server_secret_env_key} >>$secret_key_env - - public_env_key="JWT_PUBLIC=$(base64 -w 0 -i "${dir}/public")" - echo $public_env_key >$public_key_env - - echo "generated PLACE_SERVER_SECRET, JWT_SECRET and JWT_PUBLIC" - - rm -r "${dir}" -else - echo "already generated PLACE_SERVER_SECRET, JWT_SECRET and JWT_PUBLIC" -fi - -if [[ ! -f .htpasswd-kibana ]]; then - echo "$PLACE_PASSWORD" | htpasswd -i -B -c .htpasswd-kibana $PLACE_EMAIL - echo "generated kibana basic auth" -else - echo "already generated kibana basic auth" -fi - -if [[ -z "${PLACE_INSTANCE_TELEMETRY_KEY}" ]]; then - PLACE_INSTANCE_TELEMETRY_KEY="$(LOG_LEVEL=NONE task create:instance_key)" - echo "PLACE_INSTANCE_TELEMETRY_KEY=${PLACE_INSTANCE_TELEMETRY_KEY}" | grep "PLACE_INSTANCE_TELEMETRY_KEY=" >"$instance_telemetry_key_env" - echo "generated PLACE_INSTANCE_TELEMETRY_KEY" -else - echo "already generated PLACE_INSTANCE_TELEMETRY_KEY" -fi diff --git a/shard.lock b/shard.lock index 8144bc4..9b72ff3 100644 --- a/shard.lock +++ b/shard.lock @@ -69,10 +69,6 @@ shards: git: https://github.com/crystal-loot/exception_page.git version: 0.5.0 - exec_from: - git: https://github.com/place-labs/exec_from.git - version: 2.0.0 - faker: git: https://github.com/askn/faker.git version: 0.9.0 diff --git a/shard.yml b/shard.yml index bcd1459..0d0d55e 100644 --- a/shard.yml +++ b/shard.yml @@ -17,8 +17,6 @@ targets: main: src/backup.cr restore: main: src/restore.cr - exec_from: - main: lib/exec_from/src/app.cr start: main: src/start.cr @@ -36,10 +34,6 @@ dependencies: etcd: github: place-labs/crystal-etcd - exec_from: - github: place-labs/exec_from - version: ~> 2.0 - faker: github: askn/faker diff --git a/src/generate-secrets.cr b/src/generate-secrets.cr new file mode 100644 index 0000000..3b047b2 --- /dev/null +++ b/src/generate-secrets.cr @@ -0,0 +1,193 @@ +require "base64" +require "crypto/bcrypt/password" +require "openssl_ext" + +module PlaceOS::GenerateSecrets + extend self + + RED = "\033[0;31m" + GREEN = "\033[0;32m" + RESET = "\033[0m" + + INFLUX_KEY_ENV = ".env.influxdb" + CHRONOGRAF_TOKEN_SECRET_ENV = ".env.chronograf" + SECRET_KEY_ENV = ".env.secret_key" + PUBLIC_KEY_ENV = ".env.public_key" + INSTANCE_TELEMETRY_KEY_ENV = ".env.instance_telemetry_key" + ENV_FILE = ".env" + KIBANA_HTPASSWD = ".htpasswd-kibana" + + def run : Nil + env = ENV.to_h + load_env_file(INFLUX_KEY_ENV, env) + load_env_file(CHRONOGRAF_TOKEN_SECRET_ENV, env) + load_env_file(SECRET_KEY_ENV, env) + load_env_file(PUBLIC_KEY_ENV, env) + load_env_file(INSTANCE_TELEMETRY_KEY_ENV, env) + load_env_file(ENV_FILE, env, required: true) + + ensure_chronograf_secret(env) + ensure_influx_keys(env) + ensure_server_and_jwt_secrets(env) + ensure_kibana_basic_auth(env) + ensure_instance_telemetry_key(env) + end + + private def load_env_file(path : String, env : Hash(String, String), required : Bool = false) : Nil + unless File.file?(path) + abort "#{path}: No such file or directory" if required + return + end + + File.each_line(path) do |line| + parsed = parse_env_line(line) + next unless parsed + + key, value = parsed + env[key] = value + end + end + + private def parse_env_line(line : String) : {String, String}? + stripped = line.strip + return nil if stripped.empty? || stripped.starts_with?('#') + + stripped = stripped.lchop("export ").lstrip + eq_index = stripped.index('=') + return nil unless eq_index + + key = stripped[0, eq_index].strip + return nil if key.empty? + + value = stripped[eq_index + 1..].to_s.strip + + if quoted?(value, '"') + value = value[1...-1] + value = value.gsub("\\n", "\n").gsub("\\\"", "\"").gsub("\\\\", "\\") + elsif quoted?(value, '\'') + value = value[1...-1] + else + comment_index = value.index(" #") + value = value[0, comment_index] if comment_index + value = value.rstrip + end + + {key, value} + end + + private def quoted?(value : String, quote : Char) : Bool + value.size >= 2 && value.starts_with?(quote) && value.ends_with?(quote) + end + + private def present?(env : Hash(String, String), key : String) : Bool + value = env[key]? + !value.nil? && !value.empty? + end + + private def ensure_chronograf_secret(env : Hash(String, String)) : Nil + if !present?(env, "TOKEN_SECRET") + token_secret = Random::Secure.base64(256) + File.write(CHRONOGRAF_TOKEN_SECRET_ENV, "TOKEN_SECRET=#{token_secret}\n") + env["TOKEN_SECRET"] = token_secret + puts "generated Chrongraf TOKEN_SECRET" + else + puts "already generated Chrongraf TOKEN_SECRET" + end + end + + private def ensure_influx_keys(env : Hash(String, String)) : Nil + if !present?(env, "INFLUX_API_KEY") || !present?(env, "INFLUXDB_TOKEN") + influx_key = Random::Secure.base64(24) + File.write(INFLUX_KEY_ENV, "INFLUX_API_KEY=#{influx_key}\nINFLUXDB_TOKEN=#{influx_key}\n") + env["INFLUX_API_KEY"] = influx_key + env["INFLUXDB_TOKEN"] = influx_key + puts "generated INFLUX_API_KEY, INFLUXDB_TOKEN" + else + puts "already generated INFLUX_API_KEY, INFLUXDB_TOKEN" + end + end + + private def ensure_server_and_jwt_secrets(env : Hash(String, String)) : Nil + if present?(env, "PLACE_SERVER_SECRET") && present?(env, "JWT_PUBLIC") && present?(env, "JWT_SECRET") + puts "already generated PLACE_SERVER_SECRET, JWT_SECRET and JWT_PUBLIC" + return + end + + if present?(env, "PLACE_SERVER_SECRET") + puts "#{RED}ERROR#{RESET}: the #{SECRET_KEY_ENV} file contains an existing secret." + puts "Please update the file should look like the following..." + puts "JWT_SECRET=" + puts "SECRET_KEY_BASE=" + puts "PLACE_SERVER_SECRET=" + puts "SERVER_SECRET=" + exit 1 + elsif present?(env, "JWT_SECRET") && !present?(env, "PLACE_SERVER_SECRET") + puts "#{RED}ERROR#{RESET}: this instance has previously been initialised with a default secret." + puts "See the #{GREEN}server:rotate_server_secret#{RESET} task here https://github.com/PlaceOS/init#scripts" + puts "Please contact support@place.technology if you need help." + exit 1 + end + + private_key_pem = OpenSSL::PKey::RSA.new(4096).to_pem + public_key_pem = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_pem + + secret = Base64.strict_encode(private_key_pem) + jwt_public = Base64.strict_encode(public_key_pem) + secret_key_base = secret[0, 30] + + File.write(SECRET_KEY_ENV, "JWT_SECRET=#{secret}\nSECRET_KEY_BASE=#{secret_key_base}\nSERVER_SECRET=#{secret}\nPLACE_SERVER_SECRET=#{secret}\n") + File.write(PUBLIC_KEY_ENV, "JWT_PUBLIC=#{jwt_public}\n") + + env["JWT_SECRET"] = secret + env["SERVER_SECRET"] = secret + env["PLACE_SERVER_SECRET"] = secret + env["JWT_PUBLIC"] = jwt_public + + puts "generated PLACE_SERVER_SECRET, JWT_SECRET and JWT_PUBLIC" + end + + private def ensure_kibana_basic_auth(env : Hash(String, String)) : Nil + if File.file?(KIBANA_HTPASSWD) + puts "already generated kibana basic auth" + return + end + + place_password = env["PLACE_PASSWORD"]? || "" + place_email = env["PLACE_EMAIL"]? || "" + password_hash = Crypto::Bcrypt::Password.create(place_password, cost: 5).to_s + File.write(KIBANA_HTPASSWD, "#{place_email}:#{password_hash}\n") + + puts "generated kibana basic auth" + end + + private def ensure_instance_telemetry_key(env : Hash(String, String)) : Nil + if present?(env, "PLACE_INSTANCE_TELEMETRY_KEY") + puts "already generated PLACE_INSTANCE_TELEMETRY_KEY" + return + end + + output = IO::Memory.new + task_env = env.dup + task_env["LOG_LEVEL"] = "NONE" + status = Process.run("task", ["create:instance_key"], env: task_env, output: output, error: Process::Redirect::Inherit) + exit status.exit_code unless status.success? + + prefixed_output = "PLACE_INSTANCE_TELEMETRY_KEY=#{output.to_s.rstrip("\n")}" + matching_lines = [] of String + prefixed_output.each_line(chomp: true) do |line| + matching_lines << line if line.includes?("PLACE_INSTANCE_TELEMETRY_KEY=") + end + raise "failed to capture PLACE_INSTANCE_TELEMETRY_KEY" if matching_lines.empty? + + File.write(INSTANCE_TELEMETRY_KEY_ENV, "#{matching_lines.join("\n")}\n") + puts "generated PLACE_INSTANCE_TELEMETRY_KEY" + end +end + +# Keep argument behavior identical to the shell script (extra args are ignored). +begin + PlaceOS::GenerateSecrets.run +rescue error + STDERR.puts(error.message) if error.message + exit 1 +end diff --git a/src/migration.cr b/src/migration.cr index 101becf..b4e30e8 100644 --- a/src/migration.cr +++ b/src/migration.cr @@ -8,7 +8,6 @@ module Migration Ref = __FILE__ def self.raw_query(&) - # results = PlaceOS::Model::Connection.raw { |r| yield r } results = PgORM::Database.connection { |db| yield db } Log.info { results } results diff --git a/src/tasks/backup.cr b/src/tasks/backup.cr index d9f2497..29994c4 100644 --- a/src/tasks/backup.cr +++ b/src/tasks/backup.cr @@ -1,6 +1,5 @@ require "../logging" -require "exec_from" require "tasker" require "azblob" @@ -24,7 +23,7 @@ module PlaceOS::Tasks::Backup pg_db : String? = nil, pg_user : String? = nil, pg_password : String? = nil, - postfix : String = "" + postfix : String = "", ) Log.context.set( pg_host: pg_host, @@ -71,7 +70,7 @@ module PlaceOS::Tasks::Backup pg_db : String? = nil, pg_user : String? = nil, pg_password : String? = nil, - postfix : String = "" + postfix : String = "", ) Log.context.set( pg_host: pg_host, @@ -120,7 +119,7 @@ module PlaceOS::Tasks::Backup pg_user : String? = nil, pg_password : String? = nil, cron : String = BACKUP_CRON, - postfix : String = "" + postfix : String = "", ) Log.context.set( pg_host: pg_host, @@ -168,7 +167,7 @@ module PlaceOS::Tasks::Backup pg_user : String? = nil, pg_password : String? = nil, cron : String = BACKUP_CRON, - postfix : String = "" + postfix : String = "", ) Log.context.set( pg_host: pg_host, diff --git a/src/tasks/database.cr b/src/tasks/database.cr index 9ab093f..9b3bf9d 100644 --- a/src/tasks/database.cr +++ b/src/tasks/database.cr @@ -15,7 +15,7 @@ module PlaceOS::Tasks::Database pg_host : String, pg_port : Int32, pg_user : String? = nil, - pg_password : String? = nil + pg_password : String? = nil, ) pg_user = "postgres" if pg_user.nil? pg_password = "" if pg_password.nil? @@ -31,7 +31,7 @@ module PlaceOS::Tasks::Database pg_host : String, pg_port : Int32, pg_user : String? = nil, - pg_password : String? = nil + pg_password : String? = nil, ) pg_user = "postgres" if pg_user.nil? pg_password = "" if pg_password.nil? @@ -70,7 +70,7 @@ module PlaceOS::Tasks::Database pg_user : String? = nil, pg_password : String? = nil, clean_before : Bool = false, - verbose : Bool = false + verbose : Bool = false, ) pg_user = "postgres" if pg_user.nil? pg_password = "" if pg_password.nil? diff --git a/src/tasks/initialization.cr b/src/tasks/initialization.cr index 4c3197b..ad2199e 100644 --- a/src/tasks/initialization.cr +++ b/src/tasks/initialization.cr @@ -25,7 +25,7 @@ module PlaceOS::Tasks::Initialization metrics_route : String, backoffice_branch : String, backoffice_commit : String, - analytics_route : String? = nil + analytics_route : String? = nil, ) application_base = "#{tls ? "https" : "http"}://#{domain}" metrics_url = "#{application_base}/#{metrics_route}/" diff --git a/src/tasks/restore.cr b/src/tasks/restore.cr index 55c1aa1..3d782a6 100644 --- a/src/tasks/restore.cr +++ b/src/tasks/restore.cr @@ -18,7 +18,7 @@ module PlaceOS::Tasks::Restore force_restore : Bool = false, pg_user : String? = nil, pg_password : String? = nil, - aws_kms_key_id : String? = nil + aws_kms_key_id : String? = nil, ) Log.context.set({ pg_host: pg_host, @@ -68,7 +68,7 @@ module PlaceOS::Tasks::Restore force_restore : Bool = false, pg_db : String? = nil, pg_user : String? = nil, - pg_password : String? = nil + pg_password : String? = nil, ) Log.context.set( pg_host: pg_host, diff --git a/src/utils/migrate_data.cr b/src/utils/migrate_data.cr index 34f6a86..1aee8ca 100644 --- a/src/utils/migrate_data.cr +++ b/src/utils/migrate_data.cr @@ -221,6 +221,6 @@ module PlaceOS::Utils::DataMigrator attribute expires_in : Int32 attribute scopes : String attribute previous_refresh_token : String = "" - attribute created_at : Time = ->{ Time.utc }, converter: PlaceOS::Model::Timestamps::EpochConverter + attribute created_at : Time = -> { Time.utc }, converter: PlaceOS::Model::Timestamps::EpochConverter end end From 8144a489571c43c9d9fcd6ea4fec0a18f83a774a Mon Sep 17 00:00:00 2001 From: "Viv B." Date: Wed, 25 Feb 2026 11:32:58 +1100 Subject: [PATCH 2/7] chore(dockerfile): fix linter issues --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6be3897..d1713cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ RUN case "${TARGETARCH}" in \ arm64) ARCH=armv8l ;; \ *) echo "Unsupported arch: ${TARGETARCH}" && exit 1 ;; \ esac && \ - wget -O /busybox https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-${ARCH} && \ + wget --progress=dot:giga -O /busybox "https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-${ARCH}" && \ chmod +x /busybox # Create tmp directory with proper permissions @@ -101,7 +101,9 @@ SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] # chmod for setting permissions on /tmp COPY --from=build /tmp /tmp +# hadolint ignore=SC1008 - ignore shell script check RUN /bin/busybox chmod -R a+rwX /tmp +# hadolint ignore=SC1008 - ignore shell script check RUN /bin/busybox rm -rf /bin/busybox # Copy the app into place From aa6057d493ce843714a7174674a5280ba947ebaa Mon Sep 17 00:00:00 2001 From: "Viv B." Date: Wed, 25 Feb 2026 11:42:00 +1100 Subject: [PATCH 3/7] chore(dockerfile): remove lint ignore lines --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d1713cf..80dc0d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -101,9 +101,8 @@ SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] # chmod for setting permissions on /tmp COPY --from=build /tmp /tmp -# hadolint ignore=SC1008 - ignore shell script check + RUN /bin/busybox chmod -R a+rwX /tmp -# hadolint ignore=SC1008 - ignore shell script check RUN /bin/busybox rm -rf /bin/busybox # Copy the app into place From 1376186847154480802e030acd4f752ca334098d Mon Sep 17 00:00:00 2001 From: "Viv B." Date: Wed, 25 Feb 2026 11:56:42 +1100 Subject: [PATCH 4/7] chore(dockerfile): ignore shellcheck for busybox binary commands --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 80dc0d9..3dfd31c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,7 +102,9 @@ SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] # chmod for setting permissions on /tmp COPY --from=build /tmp /tmp +# shellcheck disable=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox chmod -R a+rwX /tmp +# shellcheck disable=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox rm -rf /bin/busybox # Copy the app into place From 7917e48af9335c2b19cc26aaf27f81f470b1cc60 Mon Sep 17 00:00:00 2001 From: "Viv B." Date: Wed, 25 Feb 2026 12:02:01 +1100 Subject: [PATCH 5/7] chore(dockerfile): hadolint ignore instead of shellcheck disable --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3dfd31c..10800b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,9 +102,9 @@ SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] # chmod for setting permissions on /tmp COPY --from=build /tmp /tmp -# shellcheck disable=SC1008 # ignore false positive - "This shebang was unrecognized" +# hadolint ignore=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox chmod -R a+rwX /tmp -# shellcheck disable=SC1008 # ignore false positive - "This shebang was unrecognized" +# hadolint ignore=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox rm -rf /bin/busybox # Copy the app into place From 63024da0bc10ac3b03e6110f62e58b58610d7ab5 Mon Sep 17 00:00:00 2001 From: viv-4 Date: Wed, 25 Feb 2026 12:45:28 +1100 Subject: [PATCH 6/7] fix(tasks): backup/restore exit 1 on psql failure --- src/tasks/backup.cr | 2 ++ src/tasks/restore.cr | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/tasks/backup.cr b/src/tasks/backup.cr index 29994c4..1cfed39 100644 --- a/src/tasks/backup.cr +++ b/src/tasks/backup.cr @@ -55,6 +55,7 @@ module PlaceOS::Tasks::Backup writer.write_file(path) else Log.error { "failed to capture postgresql backup" } + exit(1) end end @@ -102,6 +103,7 @@ module PlaceOS::Tasks::Backup end else Log.error { "failed to capture postgresql backup" } + exit(1) end end diff --git a/src/tasks/restore.cr b/src/tasks/restore.cr index 3d782a6..037742c 100644 --- a/src/tasks/restore.cr +++ b/src/tasks/restore.cr @@ -46,7 +46,7 @@ module PlaceOS::Tasks::Restore end Log.info { "restoring PostgreSQL DB" } - PlaceOS::Utils::PostgresDB.restore( + success = PlaceOS::Utils::PostgresDB.restore( path: Path[file.path], host: pg_host, port: pg_port, @@ -54,7 +54,14 @@ module PlaceOS::Tasks::Restore user: pg_user.not_nil!, password: pg_password.not_nil!, force_restore: force_restore, - ).tap { Log.info { "successfully restored PostgreSQL DB" } } + ) + + if success + Log.info { "successfully restored PostgreSQL DB" } + else + Log.error { "failed to restore PostgreSQL DB" } + exit(1) + end end def az_restore( @@ -91,7 +98,7 @@ module PlaceOS::Tasks::Restore end Log.info { "restoring PostgreSQL DB" } - PlaceOS::Utils::PostgresDB.restore( + success = PlaceOS::Utils::PostgresDB.restore( path: Path[file.path], host: pg_host, port: pg_port, @@ -99,6 +106,13 @@ module PlaceOS::Tasks::Restore user: pg_user.not_nil!, password: pg_password.not_nil!, force_restore: force_restore, - ).tap { Log.info { "successfully restored PostgreSQL DB" } } + ) + + if success + Log.info { "successfully restored PostgreSQL DB" } + else + Log.error { "failed to restore PostgreSQL DB" } + exit(1) + end end end From 300e4cea2557a1a8598f1c1bd971a783e9925085 Mon Sep 17 00:00:00 2001 From: "Viv B." Date: Wed, 25 Feb 2026 15:23:55 +1100 Subject: [PATCH 7/7] chore(dockerfile): remove lint ignore --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 10800b8..80dc0d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,9 +102,7 @@ SHELL ["/bin/busybox", "sh", "-euo", "pipefail", "-c"] # chmod for setting permissions on /tmp COPY --from=build /tmp /tmp -# hadolint ignore=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox chmod -R a+rwX /tmp -# hadolint ignore=SC1008 # ignore false positive - "This shebang was unrecognized" RUN /bin/busybox rm -rf /bin/busybox # Copy the app into place