@@ -57,8 +57,15 @@ backup_runtime_override="$state_root/compose.backup.yaml"
5757run_id=" $( date -u +%Y%m%dT%H%M%SZ) -${CLICKCLACK_SOURCE_COMMIT: 0: 12} "
5858log_file=" $log_root /$run_id .log"
5959stage=initialize
60+ pre_update_backup=false
61+ captured_backup_path=
62+ captured_backup_sha=
63+ captured_backup_size=
64+ captured_backup_key=
65+ captured_backup_head=
6066readonly aws_cli_version=2.35.20
6167readonly aws_cli_archive_sha256=58799ce9276d4e8815fd19e4dc35649626c6b4fbd4d0e3df7433af9cfde41882
68+ readonly max_single_put_bytes=5000000000
6269
6370install -d -m 0750 " $owner_root " " $release_root " " $state_root " " $log_root " " $( dirname " $runtime_env " ) "
6471touch " $log_file "
@@ -131,7 +138,7 @@ verify_persistent_runtime_config() {
131138 [[ -f " $runtime_env " && -f " $runtime_override " ]] || return 1
132139 configured_images=" $( grep -c ' ^ image:' " $runtime_override " || true) "
133140 [[ " $configured_images " == " 2" ]] || return 1
134- if [[ " $action " == " backup" ]]; then
141+ if [[ " $action " == " backup" || " $pre_update_backup " == " true " ]]; then
135142 grep -Eq " ^CLICKCLACK_WEB_VERSION=($requested_source_commit |$runtime_source_commit )$" " $runtime_env " || return 1
136143 requested_images=" $( grep -Fxc " image: \" clickclack:fakeco-$requested_source_commit \" " " $runtime_override " || true) "
137144 if [[ " $requested_source_commit " == " $runtime_source_commit " ]]; then
@@ -407,44 +414,142 @@ cleanup_run_files() {
407414 fi
408415}
409416
410- create_backup () {
411- stage=sqlite-backup
412- local container_path mount_path host_path integrity backup_sha backup_size backup_key backup_head manifest_key log_key
413- container_path=" /app/data/backups/clickclack-$run_id .db"
417+ capture_backup () {
418+ local backup_id=" $1 "
419+ local stage_prefix=" $2 "
420+ local container_path mount_path integrity
421+ [[ " $backup_id " =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}$ ]]
422+ [[ -z " $stage_prefix " || " $stage_prefix " == " pre-update-" ]]
423+ stage=" ${stage_prefix} sqlite-backup"
424+ container_path=" /app/data/backups/clickclack-$backup_id .db"
414425 compose exec -T app sh -c ' mkdir -p /app/data/backups'
415426 compose exec -T app clickclack backup --data /app/data --out " $container_path "
416427 mount_path=" $( docker volume inspect clickclack-fakeco-data --format ' {{.Mountpoint}}' ) "
417- host_path =" $mount_path /backups/clickclack-$run_id .db"
418- [[ -f " $host_path " ]]
419- integrity=" $( sqlite3 " $host_path " ' PRAGMA integrity_check;' ) "
428+ captured_backup_path =" $mount_path /backups/clickclack-$backup_id .db"
429+ [[ -f " $captured_backup_path " ]]
430+ integrity=" $( sqlite3 " $captured_backup_path " ' PRAGMA integrity_check;' ) "
420431 [[ " $integrity " == " ok" ]]
421- backup_sha=" $( sha256sum " $host_path " | cut -d' ' -f1) "
422- backup_size=" $( stat -c ' %s' " $host_path " ) "
423- backup_key=" $CLICKCLACK_BACKUP_PREFIX /sqlite/$runtime_source_commit /clickclack-$run_id .db"
424- backup_head=" $state_root /backup-head-$run_id .json"
425- manifest_key=" $CLICKCLACK_BACKUP_PREFIX /manifests/$run_id .json"
426- log_key=" $CLICKCLACK_LOG_PREFIX /runs/$run_id /owner.log"
432+ captured_backup_sha=" $( sha256sum " $captured_backup_path " | cut -d' ' -f1) "
433+ captured_backup_size=" $( stat -c ' %s' " $captured_backup_path " ) "
434+ [[ " $captured_backup_size " =~ ^[0-9]+$ ]]
435+ if (( captured_backup_size > max_single_put_bytes)) ; then
436+ printf ' backup size exceeds the FakeCo single-object limit (%s bytes)\n' \
437+ " $max_single_put_bytes " >&2
438+ return 1
439+ fi
440+ captured_backup_key=" $CLICKCLACK_BACKUP_PREFIX /sqlite/$runtime_source_commit /clickclack-$backup_id .db"
441+ captured_backup_head=" $state_root /backup-head-$backup_id .json"
427442
428- stage=upload-backup
429- aws s3 cp " $host_path " " s3://$CLICKCLACK_BACKUP_BUCKET /$backup_key " \
430- --only-show-errors \
431- --metadata " sha256=$backup_sha ,source-commit=$runtime_source_commit " \
432- --sse aws:kms \
433- --sse-kms-key-id " $CLICKCLACK_DATA_KMS_KEY_ARN "
443+ stage=" ${stage_prefix} upload-backup"
444+ aws s3api put-object \
445+ --bucket " $CLICKCLACK_BACKUP_BUCKET " \
446+ --key " $captured_backup_key " \
447+ --body " $captured_backup_path " \
448+ --content-type application/vnd.sqlite3 \
449+ --metadata " sha256=$captured_backup_sha ,source-commit=$runtime_source_commit " \
450+ --server-side-encryption aws:kms \
451+ --ssekms-key-id " $CLICKCLACK_DATA_KMS_KEY_ARN " \
452+ --if-none-match ' *' \
453+ --output json > /dev/null
434454 aws s3api head-object \
435455 --bucket " $CLICKCLACK_BACKUP_BUCKET " \
436- --key " $backup_key " \
437- --output json > " $backup_head "
456+ --key " $captured_backup_key " \
457+ --output json > " $captured_backup_head "
438458 jq -e \
439- --arg sha256 " $backup_sha " \
459+ --arg sha256 " $captured_backup_sha " \
440460 --arg source_commit " $runtime_source_commit " \
441461 --arg kms_key " $CLICKCLACK_DATA_KMS_KEY_ARN " \
442- --argjson size " $backup_size " \
462+ --argjson size " $captured_backup_size " \
443463 ' .Metadata.sha256 == $sha256 and
444464 .Metadata["source-commit"] == $source_commit and
445465 .ServerSideEncryption == "aws:kms" and
446466 .SSEKMSKeyId == $kms_key and
447- .ContentLength == $size' " $backup_head " > /dev/null
467+ .ContentLength == $size' " $captured_backup_head " > /dev/null
468+ }
469+
470+ create_pre_update_backup () {
471+ local backup_id backup_suffix manifest_key evidence_file evidence_sha evidence_size evidence_head
472+ backup_suffix=" $( printf ' pre-update:%s:%s:%s' \
473+ " $run_id " " $runtime_source_commit " " $CLICKCLACK_OWNER_COMMIT " | sha256sum | cut -c1-12) "
474+ backup_id=" ${run_id% -* } -$backup_suffix "
475+ [[ " $backup_id " != " $run_id " ]]
476+ capture_backup " $backup_id " pre-update-
477+ manifest_key=" $CLICKCLACK_BACKUP_PREFIX /manifests/$backup_id .json"
478+ evidence_file=" $state_root /pre-update-evidence-$run_id .json"
479+ evidence_head=" $state_root /pre-update-evidence-head-$run_id .json"
480+ jq -n \
481+ --arg action pre-update \
482+ --arg run_id " $backup_id " \
483+ --arg source_commit " $runtime_source_commit " \
484+ --arg requested_source_commit " $requested_source_commit " \
485+ --arg owner_commit " $CLICKCLACK_OWNER_COMMIT " \
486+ --arg image_id " $image_id " \
487+ --arg backup_bucket " $CLICKCLACK_BACKUP_BUCKET " \
488+ --arg backup_key " $captured_backup_key " \
489+ --arg backup_sha256 " $captured_backup_sha " \
490+ --argjson backup_size " $captured_backup_size " \
491+ --arg manifest_bucket " $CLICKCLACK_BACKUP_BUCKET " \
492+ --arg manifest_key " $manifest_key " \
493+ ' {
494+ schema_version: 1,
495+ status: "passed",
496+ action: $action,
497+ run_id: $run_id,
498+ source_commit: $source_commit,
499+ requested_source_commit: $requested_source_commit,
500+ owner_commit: $owner_commit,
501+ runtime_commit_verified: true,
502+ image_id: $image_id,
503+ health: true,
504+ readiness: true,
505+ metrics_metadata_only: true,
506+ integrity_check: "ok",
507+ backup: {
508+ bucket: $backup_bucket,
509+ key: $backup_key,
510+ sha256: $backup_sha256,
511+ size_bytes: $backup_size
512+ },
513+ manifest: {bucket: $manifest_bucket, key: $manifest_key}
514+ }' > " $evidence_file "
515+ evidence_sha=" $( sha256sum " $evidence_file " | cut -d' ' -f1) "
516+ evidence_size=" $( stat -c ' %s' " $evidence_file " ) "
517+
518+ stage=pre-update-upload-evidence
519+ aws s3api put-object \
520+ --bucket " $CLICKCLACK_BACKUP_BUCKET " \
521+ --key " $manifest_key " \
522+ --body " $evidence_file " \
523+ --content-type application/json \
524+ --metadata " sha256=$evidence_sha ,source-commit=$runtime_source_commit ,requested-source-commit=$requested_source_commit " \
525+ --server-side-encryption aws:kms \
526+ --ssekms-key-id " $CLICKCLACK_DATA_KMS_KEY_ARN " \
527+ --if-none-match ' *' \
528+ --output json > /dev/null
529+ aws s3api head-object \
530+ --bucket " $CLICKCLACK_BACKUP_BUCKET " \
531+ --key " $manifest_key " \
532+ --output json > " $evidence_head "
533+ jq -e \
534+ --arg sha256 " $evidence_sha " \
535+ --arg source_commit " $runtime_source_commit " \
536+ --arg requested_source_commit " $requested_source_commit " \
537+ --arg kms_key " $CLICKCLACK_DATA_KMS_KEY_ARN " \
538+ --argjson size " $evidence_size " \
539+ ' .Metadata.sha256 == $sha256 and
540+ .Metadata["source-commit"] == $source_commit and
541+ .Metadata["requested-source-commit"] == $requested_source_commit and
542+ .ServerSideEncryption == "aws:kms" and
543+ .SSEKMSKeyId == $kms_key and
544+ .ContentLength == $size' " $evidence_head " > /dev/null
545+ rm -f -- " $captured_backup_path " " $captured_backup_head " " $evidence_file " " $evidence_head "
546+ }
547+
548+ create_backup () {
549+ local manifest_key log_key
550+ capture_backup " $run_id " " "
551+ manifest_key=" $CLICKCLACK_BACKUP_PREFIX /manifests/$run_id .json"
552+ log_key=" $CLICKCLACK_LOG_PREFIX /runs/$run_id /owner.log"
448553 compose logs --no-color --tail 500 app > " $state_root /app-$run_id .log"
449554
450555 evidence_file=" $state_root /evidence-$run_id .json"
@@ -457,8 +562,8 @@ create_backup() {
457562 --arg image_id " $image_id " \
458563 --arg seed_sha256 " $seed_sha256 " \
459564 --arg backup_bucket " $CLICKCLACK_BACKUP_BUCKET " \
460- --arg backup_key " $backup_key " \
461- --arg backup_sha256 " $backup_sha " \
565+ --arg backup_key " $captured_backup_key " \
566+ --arg backup_sha256 " $captured_backup_sha " \
462567 --arg manifest_bucket " $CLICKCLACK_BACKUP_BUCKET " \
463568 --arg manifest_key " $manifest_key " \
464569 --arg log_bucket " $CLICKCLACK_LOG_BUCKET " \
@@ -485,12 +590,16 @@ create_backup() {
485590 }' > " $evidence_file "
486591
487592 stage=upload-evidence
488- aws s3 cp " $evidence_file " " s3://$CLICKCLACK_BACKUP_BUCKET /$manifest_key " \
489- --only-show-errors \
593+ aws s3api put-object \
594+ --bucket " $CLICKCLACK_BACKUP_BUCKET " \
595+ --key " $manifest_key " \
596+ --body " $evidence_file " \
490597 --content-type application/json \
491- --sse aws:kms \
492- --sse-kms-key-id " $CLICKCLACK_DATA_KMS_KEY_ARN "
493- cleanup_success " $host_path "
598+ --server-side-encryption aws:kms \
599+ --ssekms-key-id " $CLICKCLACK_DATA_KMS_KEY_ARN " \
600+ --if-none-match ' *' \
601+ --output json > /dev/null
602+ cleanup_success " $captured_backup_path "
494603 stage=upload-log
495604 aws s3 cp " $log_file " " s3://$CLICKCLACK_LOG_BUCKET /$log_key " \
496605 --only-show-errors \
@@ -501,7 +610,43 @@ create_backup() {
501610 cat " $evidence_file " >&3
502611}
503612
613+ runtime_state_exists () {
614+ local containers
615+ if [[ -e " $runtime_env " || -e " $runtime_override " || -e /var/lib/docker/volumes/clickclack-fakeco-data ]]; then
616+ return 0
617+ fi
618+ if command -v docker > /dev/null && systemctl is-active --quiet docker; then
619+ containers=" $( docker ps -a \
620+ --filter ' label=com.docker.compose.project=clickclack-fakeco' \
621+ --filter ' label=com.docker.compose.service=app' \
622+ --format ' {{.ID}}' ) "
623+ [[ -n " $containers " ]] && return 0
624+ fi
625+ return 1
626+ }
627+
628+ prepare_pre_update_backup () {
629+ stage=pre-update-detection
630+ runtime_state_exists || return 0
631+ [[ -f " $runtime_env " && ! -L " $runtime_env " ]]
632+ [[ -f " $runtime_override " && ! -L " $runtime_override " ]]
633+ systemctl is-active --quiet docker
634+ pre_update_backup=true
635+ resolve_backup_runtime
636+ verify_running_image
637+ probe_service
638+ create_pre_update_backup
639+ rm -f -- " $backup_runtime_override "
640+ pre_update_backup=false
641+ runtime_source_commit=" $requested_source_commit "
642+ runtime_source_sha256=" $CLICKCLACK_SOURCE_SHA256 "
643+ compose_override=" $runtime_override "
644+ set_runtime_paths
645+ stage=pre-update-complete
646+ }
647+
504648if [[ " $action " == " bootstrap" ]]; then
649+ prepare_pre_update_backup
505650 install_runtime
506651 install_source
507652 write_runtime_config
0 commit comments