package_offline.sh 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. # shellcheck source=wp13-common.sh
  5. source "${SCRIPT_DIR}/wp13-common.sh"
  6. usage() {
  7. cat <<'EOF'
  8. Usage: package_offline.sh [options]
  9. Build a checksum-verifiable DataOps Compose offline archive.
  10. Options:
  11. --repo-root PATH Repository root.
  12. --output-dir PATH Destination directory (default: <repo>/dist).
  13. --version VALUE Package version (default: source commit UTC timestamp).
  14. --source-date-epoch N Reproducible build timestamp (default: source commit time).
  15. --skip-images Contract-test mode; omit Docker image archive.
  16. --help Show this help.
  17. EOF
  18. }
  19. REPO_ROOT="$(wp13_default_root)"
  20. OUTPUT_DIR=""
  21. VERSION=""
  22. SOURCE_DATE_EPOCH=""
  23. SKIP_IMAGES=0
  24. while [[ $# -gt 0 ]]; do
  25. case "$1" in
  26. --repo-root) REPO_ROOT="$2"; shift 2 ;;
  27. --output-dir) OUTPUT_DIR="$2"; shift 2 ;;
  28. --version) VERSION="$2"; shift 2 ;;
  29. --source-date-epoch) SOURCE_DATE_EPOCH="$2"; shift 2 ;;
  30. --skip-images) SKIP_IMAGES=1; shift ;;
  31. --help|-h) usage; exit 0 ;;
  32. *) wp13_die "unknown argument: $1" ;;
  33. esac
  34. done
  35. REPO_ROOT="$(cd "${REPO_ROOT}" && pwd)"
  36. OUTPUT_DIR="${OUTPUT_DIR:-${REPO_ROOT}/dist}"
  37. mkdir -p "${OUTPUT_DIR}"
  38. OUTPUT_DIR="$(cd "${OUTPUT_DIR}" && pwd)"
  39. wp13_require_command python3
  40. SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git -C "${REPO_ROOT}" show -s --format=%ct HEAD 2>/dev/null || printf 0)}"
  41. [[ "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]] \
  42. || wp13_die "--source-date-epoch must be a non-negative integer"
  43. VERSION="${VERSION:-$(python3 -c 'import datetime,sys; print(datetime.datetime.fromtimestamp(int(sys.argv[1]), datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ"))' "${SOURCE_DATE_EPOCH}")}"
  44. for required in \
  45. deploy/docker/docker-compose.yml \
  46. deploy/docker/.env.example \
  47. database \
  48. migrations \
  49. deployment/environments \
  50. deployment/helm/dataops-platform \
  51. deployment/enterprise \
  52. docs/architecture/OPENAPI.yaml \
  53. deployment/compose; do
  54. [[ -e "${REPO_ROOT}/${required}" ]] \
  55. || wp13_die "required release input is missing: ${required}"
  56. done
  57. if ! diff -qr \
  58. --exclude='__pycache__' \
  59. --exclude='*.pyc' \
  60. "${REPO_ROOT}/app" "${REPO_ROOT}/deployment/app" >/dev/null; then
  61. wp13_die "deployment/app is not synchronised with app"
  62. fi
  63. if ! diff -qr \
  64. --exclude='__pycache__' \
  65. --exclude='*.pyc' \
  66. "${REPO_ROOT}/migrations" "${REPO_ROOT}/deployment/migrations" >/dev/null; then
  67. wp13_die "deployment/migrations is not synchronised with migrations"
  68. fi
  69. PACKAGE_NAME="dataops-platform-offline-${VERSION}"
  70. TEMP_DIR="$(mktemp -d)"
  71. trap 'rm -rf "${TEMP_DIR}"' EXIT
  72. BUNDLE_ROOT="${TEMP_DIR}/${PACKAGE_NAME}"
  73. mkdir -p \
  74. "${BUNDLE_ROOT}/deploy" \
  75. "${BUNDLE_ROOT}/environments" \
  76. "${BUNDLE_ROOT}/operations" \
  77. "${BUNDLE_ROOT}/helm" \
  78. "${BUNDLE_ROOT}/enterprise" \
  79. "${BUNDLE_ROOT}/images" \
  80. "${BUNDLE_ROOT}/trace"
  81. cp -R "${REPO_ROOT}/deploy/docker" "${BUNDLE_ROOT}/deploy/docker"
  82. cp -R "${REPO_ROOT}/database" "${BUNDLE_ROOT}/database"
  83. cp -R "${REPO_ROOT}/migrations" "${BUNDLE_ROOT}/migrations"
  84. cp -R "${REPO_ROOT}/deployment/environments/." "${BUNDLE_ROOT}/environments/"
  85. cp -R "${REPO_ROOT}/deployment/compose/." "${BUNDLE_ROOT}/operations/"
  86. cp -R "${REPO_ROOT}/deployment/helm/dataops-platform" "${BUNDLE_ROOT}/helm/dataops-platform"
  87. cp -R "${REPO_ROOT}/deployment/enterprise/." "${BUNDLE_ROOT}/enterprise/"
  88. cp "${REPO_ROOT}/deployment/MANIFEST.md" "${BUNDLE_ROOT}/MANIFEST.md"
  89. cp "${REPO_ROOT}/docs/architecture/OPENAPI.yaml" "${BUNDLE_ROOT}/trace/OPENAPI.yaml"
  90. find "${BUNDLE_ROOT}" -type d -name __pycache__ -prune -exec rm -rf {} +
  91. find "${BUNDLE_ROOT}" -type f -name '*.pyc' -delete
  92. COMPOSE_FILE="${BUNDLE_ROOT}/deploy/docker/docker-compose.yml"
  93. IMAGE_LIST="${BUNDLE_ROOT}/images/images.txt"
  94. wp13_require_command docker
  95. docker compose -f "${REPO_ROOT}/deploy/docker/docker-compose.yml" \
  96. config --images | LC_ALL=C sort -u >"${IMAGE_LIST}"
  97. python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" sbom \
  98. --repo-root "${REPO_ROOT}" \
  99. --output-dir "${BUNDLE_ROOT}/trace"
  100. python3 "${REPO_ROOT}/deployment/compose/product_engineering.py" manifest \
  101. --repo-root "${REPO_ROOT}" \
  102. --version "${VERSION}" \
  103. --source-date-epoch "${SOURCE_DATE_EPOCH}" \
  104. --output "${BUNDLE_ROOT}/release-manifest.json"
  105. CREATED_AT_UTC="$(python3 -c 'import datetime,sys; print(datetime.datetime.fromtimestamp(int(sys.argv[1]), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))' "${SOURCE_DATE_EPOCH}")"
  106. {
  107. printf 'version=%s\n' "${VERSION}"
  108. printf 'created_at_utc=%s\n' "${CREATED_AT_UTC}"
  109. printf 'source_date_epoch=%s\n' "${SOURCE_DATE_EPOCH}"
  110. printf 'source_commit=%s\n' "$(git -C "${REPO_ROOT}" rev-parse HEAD 2>/dev/null || printf unknown)"
  111. printf 'source_tree=%s\n' "$(git -C "${REPO_ROOT}" rev-parse 'HEAD^{tree}' 2>/dev/null || printf unknown)"
  112. printf 'compose_sha256=%s\n' "$(wp13_sha256_file "${COMPOSE_FILE}")"
  113. printf 'release_manifest_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/release-manifest.json")"
  114. printf 'openapi_sha256=%s\n' "$(wp13_sha256_file "${BUNDLE_ROOT}/trace/OPENAPI.yaml")"
  115. printf 'images_included=%s\n' "$([[ "${SKIP_IMAGES}" -eq 1 ]] && printf no || printf yes)"
  116. while IFS= read -r image; do
  117. [[ -n "${image}" ]] || continue
  118. image_id="$(docker image inspect -f '{{.Id}}' "${image}" 2>/dev/null || printf missing)"
  119. printf 'image=%s|%s\n' "${image}" "${image_id}"
  120. done <"${IMAGE_LIST}"
  121. } >"${BUNDLE_ROOT}/release-manifest.txt"
  122. if [[ "${SKIP_IMAGES}" -eq 0 ]]; then
  123. while IFS= read -r image; do
  124. [[ -n "${image}" ]] || continue
  125. docker image inspect "${image}" >/dev/null 2>&1 \
  126. || wp13_die "cannot package missing image: ${image}"
  127. done <"${IMAGE_LIST}"
  128. # shellcheck disable=SC2046
  129. docker save -o "${BUNDLE_ROOT}/images/dataops-images.tar" $(cat "${IMAGE_LIST}")
  130. fi
  131. wp13_write_checksums "${BUNDLE_ROOT}"
  132. ARCHIVE="${OUTPUT_DIR}/${PACKAGE_NAME}.tar.gz"
  133. python3 "${REPO_ROOT}/deployment/compose/reproducible_archive.py" \
  134. --source "${BUNDLE_ROOT}" \
  135. --output "${ARCHIVE}" \
  136. --source-date-epoch "${SOURCE_DATE_EPOCH}"
  137. wp13_log "offline package created: ${ARCHIVE}"