fork 0k-charms
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

539 lines
20 KiB

  1. #!/bin/bash
  2. ## compose: no-hooks
  3. if [ -z "$SERVICE_DATASTORE" ]; then
  4. echo "This script is meant to be run through 'compose' to work properly." >&2
  5. exit 1
  6. fi
  7. version=0.1
  8. usage="$exname [-h|--help] [--force|-f] [TARGET_VERSION]"
  9. help="
  10. USAGE:
  11. $usage
  12. DESCRIPTION:
  13. Migrate the current $SERVICE_NAME service to given target version. Don't
  14. forget to change your \`\`compose.yml\`\` accordingly afterwards.
  15. EXAMPLES:
  16. $exname 21.0.0
  17. "
  18. no_hint=
  19. force=
  20. target=
  21. while [ "$1" ]; do
  22. case "$1" in
  23. "--help"|"-h")
  24. print_help >&2
  25. exit 0
  26. ;;
  27. "--force"|"-f")
  28. force=yes
  29. ;;
  30. "--color"|"-c")
  31. ansi_color yes
  32. ;;
  33. "--no-hint")
  34. no_hint=yes
  35. ;;
  36. --*|-*)
  37. err "Unexpected optional argument '$1'"
  38. print_usage >&2
  39. exit 1
  40. ;;
  41. *)
  42. [ -z "$target" ] && { target=$1 ; shift ; continue ; }
  43. err "Unexpected positional argument '$1'"
  44. print_usage >&2
  45. exit 1
  46. ;;
  47. esac
  48. shift
  49. done
  50. postgres:image:version() {
  51. local image="$1" image_version
  52. ## XXXvlab: why not always use the pragmatic second method ?
  53. if [[ "${image}" =~ ^.*:[0-9]+.[0-9]+.[0-9]+$ ]]; then
  54. debug "Infering postgres's version from image's name $image"
  55. image_version="${image#*:}"
  56. image_version="${image_version%-myc}"
  57. else
  58. debug "Looking for postgres's version in image $image"
  59. if ! out=$(docker run --rm -i --entrypoint postgres "$image" --version); then
  60. err "Couldn't infer image '$image' postgres's version."
  61. exit 1
  62. fi
  63. ## Expected `$out` content of the form of: 'postgres (PostgreSQL) 10.14'
  64. out=${out%%$'\n'*}
  65. out=${out%%$'\r'*}
  66. image_version=${out#"postgres (PostgreSQL) "}.0
  67. ## check if this is a version
  68. if ! [[ "$image_version" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then
  69. err "Couldn't infer image '$image' postgres's version: invalid version."
  70. exit 1
  71. fi
  72. fi
  73. echo "$image_version"
  74. }
  75. postgres:container:version() {
  76. local container="$1"
  77. if ! out=$(docker exec -i "$container" postgres --version); then
  78. err "Couldn't infer container's '$container' postgres's version."
  79. exit 1
  80. fi
  81. out=${out%%$'\n'*}
  82. out=${out%%$'\r'*}
  83. image_version=${out#"postgres (PostgreSQL) "}.0
  84. ## check if this is a version
  85. if ! [[ "$image_version" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then
  86. err "Couldn't infer image '$image' postgres's version: invalid version '$image_version'."
  87. exit 1
  88. fi
  89. echo "$image_version"
  90. }
  91. postgres:container:psql () {
  92. local container="$1"
  93. docker exec -i "$container" psql 2>&1
  94. }
  95. current_image_version=$(postgres:image:version "${DOCKER_BASE_IMAGE}") || exit 1
  96. if ! [[ "$current_image_version" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then
  97. err "Current postgres version ${WHITE}$current_image_version${NORMAL} is unsupported yet."
  98. exit 1
  99. fi
  100. starting_image_version=$current_image_version
  101. ## This next line has to be on stdout, it is used by `vps` to get information
  102. info "Current postgres version: ${WHITE}$current_image_version${NORMAL}" 2>&1
  103. last_available_versions=(
  104. $(DEBUG= docker:tags:fetch docker.0k.io/postgres 30 '[0-9]+\.[0-9]+\.[0-9]+-myc$' |
  105. sed -r 's/-myc$//g' |
  106. sort -rV)
  107. )
  108. debug "Last available versions: ${WHITE}${last_available_versions[*]}${NORMAL}"
  109. if [ -z "$target" ]; then
  110. info "Latest version available: ${WHITE}${last_available_versions[0]}${NORMAL}"
  111. fi
  112. ## XXXvlab: put this in kal-shlib-common
  113. version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
  114. last_upgradable_versions=()
  115. for v in "${last_available_versions[@]}"; do
  116. if version_gt "$v" "$current_image_version"; then
  117. last_upgradable_versions+=("$v")
  118. fi
  119. done
  120. if [ "${#last_upgradable_versions[@]}" == 0 ]; then
  121. if [ -n "$target" ] && [ "$target" != "$current_image_version" ]; then
  122. warn "Provided target version ${WHITE}$target${NORMAL} is likely incorrect."
  123. echo " It is either non-existent and/or inferior to current version." >&2
  124. fi
  125. ## This next line has to be on stdout, it is used by `vps` to get information
  126. info "${DARKYELLOW}postgres${NORMAL} is already ${GREEN}up-to-date${NORMAL}." 2>&1
  127. exit 0
  128. fi
  129. if [ -z "$target" ]; then
  130. info "Target latest version: ${WHITE}${last_upgradable_versions[0]}${NORMAL}"
  131. target=${last_upgradable_versions[0]}
  132. else
  133. if [[ " ${last_available_versions[*]} " != *" $target "* ]]; then
  134. err "Invalid version $target selected, please specify one of:"
  135. for v in "${last_upgradable_versions[@]}"; do
  136. echo " - $v"
  137. done >&2
  138. exit 1
  139. fi
  140. last_upgradable_versions_filtered=()
  141. info "Target version ${WHITE}$target${NORMAL}"
  142. for v in "${last_upgradable_versions[@]}"; do
  143. if [ "$target" == "$v" ] || version_gt "$target" "$v"; then
  144. last_upgradable_versions_filtered+=("$v")
  145. fi
  146. done
  147. last_upgradable_versions=("${last_upgradable_versions_filtered[@]}")
  148. fi
  149. debug "Last upgradable versions: ${WHITE}${last_upgradable_versions[*]}${NORMAL}"
  150. upgrade_path=($(printf "%s\n" "${last_upgradable_versions[@]}" | sort -V | tail -n 1))
  151. containers="$(get_running_containers_for_service "$SERVICE_NAME")"
  152. debug "Running containers for service $SERVICE_NAME: $containers"
  153. debug "Upgrade path: ${WHITE}${upgrade_path[@]}${NORMAL}"
  154. container_stopped=()
  155. if [ -n "$containers" ]; then
  156. #err "Running container(s) for $DARKYELLOW$SERVICE_NAME$NORMAL are still running:"
  157. for container in $containers; do
  158. debug "Stopping container $container"
  159. docker stop "$container" >/dev/null || {
  160. err "Failed to stop container '$container'."
  161. exit 1
  162. }
  163. ## We need to delete it as otherwise, on final ``compose --debug up``
  164. ## it won't be recreated, and it will not be the correct version.
  165. docker rm "$container" > /dev/null || {
  166. err "Couldn't delete container '$container'."
  167. }
  168. container_stopped+=("$container")
  169. done
  170. fi
  171. ## XXXvlab: taking first container is probably not a good idea
  172. container="$(echo "$containers" | head -n 1)"
  173. settmpdir MIGRATION_TMPDIR
  174. debug "Migration temporary directory: $MIGRATION_TMPDIR"
  175. set -o pipefail
  176. (
  177. . "$CHARM_PATH/lib/common"
  178. cp "$LOCAL_DB_PASSFILE" "$MIGRATION_TMPDIR/pgpass"
  179. )
  180. current_image="docker.0k.io/postgres:${current_image_version}-myc"
  181. if ! docker_has_image "$current_image"; then
  182. out=$(docker pull "$current_image" 2>&1) || {
  183. err "Couldn't pull image $current_image:"
  184. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} " >&2
  185. exit 1
  186. }
  187. fi
  188. migration_backup_dirs=()
  189. ## In postgres, it seems it is not useful to have a path. So path is
  190. ## only one step here.
  191. failed=
  192. while [[ "${#upgrade_path[@]}" != 0 ]]; do
  193. image_version="${upgrade_path[0]}"
  194. if version_gt "$image_version" 12; then
  195. docker_version=$(docker info --format '{{.ServerVersion}}')
  196. if ! version_gt "$docker_version" 20.10.0; then
  197. err "Sorry, ${DARKYELLOW}$SERVICE_NAME${NORMAL} ${WHITE}$image_version${NORMAL}" \
  198. "require docker version >= 20.10 (current: $docker_version)"
  199. break
  200. fi
  201. fi
  202. upgrade_path=("${upgrade_path[@]:1}")
  203. ## Prevent build of image instead of pulling it
  204. while true; do
  205. info "Upgrading step ${WHITE}$current_image_version${NORMAL} => ${WHITE}$image_version${NORMAL}"
  206. rm -f "$MIGRATION_TMPDIR/dump-${current_image_version}.log" &&
  207. touch "$MIGRATION_TMPDIR/dump-${current_image_version}.log"
  208. rm -f "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log" &&
  209. touch "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log"
  210. uuid=$(openssl rand -hex 16)
  211. ## Generate dump with pg_dumpall of current version:
  212. (
  213. {
  214. cmd=(
  215. compose --no-hooks
  216. --add-compose-content="$SERVICE_NAME:
  217. docker-compose:
  218. image: docker.0k.io/postgres:${current_image_version}-myc
  219. hostname: $SERVICE_NAME
  220. "
  221. run --rm --label "migration-uuid=$uuid" "$SERVICE_NAME"
  222. )
  223. echo COMMAND: "${cmd[@]}"
  224. "${cmd[@]}" 2>&1
  225. echo "ERRORLEVEL: $?"
  226. } | tee "$MIGRATION_TMPDIR/dump-${current_image_version}.log" >/dev/null
  227. ) &
  228. pid="$!"
  229. debug "Dumping container of $SERVICE_NAME in ${WHITE}$current_image_version${NORMAL}" \
  230. "launched with PID ${YELLOW}$pid${NORMAL}"
  231. expected_stop=""
  232. unexpected_stop=""
  233. errlvl=""
  234. while read -r line; do
  235. case "$line" in
  236. *" database system is ready to accept connections"*)
  237. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  238. if [ -z "$migration_container" ]; then
  239. err "Couldn't find migration container for $current_image_version"
  240. echo " despite catched line stating postgres is listening." >&2
  241. break 3
  242. fi
  243. ## dump all
  244. debug "Start dumping postgres database of ${WHITE}$current_image_version${NORMAL}"
  245. if ! out=$(
  246. {
  247. docker exec -u 70 "${migration_container}" pg_dumpall |
  248. gzip > "$MIGRATION_TMPDIR/dump.sql.gz"
  249. } 2>&1 ); then
  250. err "Failed to dump postgres database."
  251. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  252. unexpected_stop="1"
  253. break
  254. fi
  255. out=$(docker stop "$migration_container" 2>&1) || {
  256. err "Failed to stop the dump container $migration_container for $current_image_version:"
  257. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  258. break 3
  259. }
  260. expected_stop="1"
  261. debug "Interrupting server process after dump (${YELLOW}$pid${NORMAL})" >&2
  262. kill "$pid"
  263. errlvl="0"
  264. break
  265. ;;
  266. *"getaddrinfo(\"$SERVICE_NAME\") failed: Name or service not known"*)
  267. err "Postgres issue with resolving '$SERVICE_NAME'"
  268. failed=1
  269. break 3
  270. ;;
  271. "ERRORLEVEL: "*)
  272. unexpected_stop="1"
  273. errlvl="${line#ERRORLEVEL: }"
  274. err "Unexpected stop of postgres dump container with errorlevel $errlvl"
  275. break
  276. ;;
  277. esac
  278. done < <(tail -f "$MIGRATION_TMPDIR/dump-${current_image_version}.log")
  279. [ "$unexpected_stop" == "1" ] && {
  280. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  281. if [ -n "$migration_container" ]; then
  282. err "Stopping dumping docker container '$migration_container' after failure"
  283. docker stop "$migration_container" >/dev/null 2>&1 || true
  284. fi
  285. failed=1
  286. break
  287. }
  288. [ "$expected_stop" == "1" ] && {
  289. out=$(docker rmi "$current_image" 2>&1) || {
  290. err "Failed to remove image $current_image"
  291. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  292. break 2
  293. }
  294. }
  295. next_image="docker.0k.io/postgres:${image_version}-myc"
  296. if ! docker_has_image "$next_image"; then
  297. out=$(docker pull "$next_image" 2>&1) || {
  298. err "Couldn't pull image $next_image:"
  299. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} " >&2
  300. unexpected_stop=1
  301. break 2
  302. }
  303. fi
  304. rm -f "$MIGRATION_TMPDIR/dump-${current_image_version}.log"
  305. uuid=$(openssl rand -hex 16)
  306. mv "$SERVICE_DATASTORE"{,".migration-${current_image_version}-${uuid}"} 2>&1 || {
  307. err "Couldn't move datastore to backup location for $SERVICE_NAME"
  308. unexpected_stop=1
  309. break 2
  310. }
  311. migration_backup_dirs+=("$SERVICE_DATASTORE.migration-${current_image_version}-${uuid}")
  312. (
  313. {
  314. cmd=(
  315. compose --debug
  316. --add-compose-content="$SERVICE_NAME:
  317. docker-compose:
  318. image: docker.0k.io/postgres:${image_version}-myc
  319. hostname: $SERVICE_NAME
  320. "
  321. run --rm --label "migration-uuid=$uuid" "$SERVICE_NAME"
  322. )
  323. echo COMMAND: "${cmd[@]}"
  324. "${cmd[@]}" 2>&1
  325. echo "ERRORLEVEL: $?"
  326. } | tee "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log" >/dev/null
  327. ) &
  328. pid="$!"
  329. debug "Migration launched with PID ${YELLOW}$pid${NORMAL}"
  330. expected_stop=""
  331. unexpected_stop=""
  332. errlvl=""
  333. while read -r line; do
  334. case "$line" in
  335. *" database system is ready to accept connections"*)
  336. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  337. ## dump all
  338. debug "Start restoring in postgres database ${WHITE}$current_image_version${NORMAL}"
  339. if ! out=$(
  340. {
  341. gunzip -c "$MIGRATION_TMPDIR/dump.sql.gz" |
  342. docker exec -i -u 70 "${migration_container}" psql
  343. } 2>&1 ); then
  344. err "Failed to restore postgres database."
  345. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  346. unexpected_stop="1"
  347. break
  348. fi
  349. current_image="$next_image"
  350. current_image_version="$image_version"
  351. out=$(docker stop "$migration_container" 2>&1) || {
  352. err "Failed to stop the migration container $migration_container for $image_version:"
  353. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  354. break 3
  355. }
  356. expected_stop="1"
  357. debug "Interrupting server process after migration (${YELLOW}$pid${NORMAL})" >&2
  358. kill "$pid"
  359. errlvl="0"
  360. break
  361. ;;
  362. *"getaddrinfo(\"$SERVICE_NAME\") failed: Name or service not known"*)
  363. err "Postgres issue with resolving '$SERVICE_NAME'"
  364. failed=1
  365. break 3
  366. ;;
  367. *"Replication has not yet been configured"*)
  368. warn "Postgres was not setup right away, could lead to issues"
  369. ;;
  370. "ERRORLEVEL: "*)
  371. unexpected_stop="1"
  372. errlvl="${line#ERRORLEVEL: }"
  373. err "Unexpected stop of postgres migration container with errorlevel $errlvl"
  374. failed=1
  375. break
  376. ;;
  377. esac
  378. done < <(tail -f "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log")
  379. [ "$unexpected_stop" == "1" ] && {
  380. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  381. if [ -n "$migration_container" ]; then
  382. err "Stopping dumping docker container '$migration_container' after failure"
  383. docker stop "$migration_container" >/dev/null 2>&1 || true
  384. fi
  385. failed=1
  386. break
  387. }
  388. [ "$expected_stop" == "1" ] && {
  389. continue 2
  390. }
  391. ##
  392. ## Damage control, there are some things we can solve
  393. ##
  394. if grep "UPGRADE PROBLEM: Found an invalid featureCompatibilityVersion document" "$MIGRATION_TMPDIR/migration.log" >/dev/null 2>&1; then
  395. if [ -z "$feature_comp" ]; then
  396. feature_comp=1
  397. info "Detected featureCompatibilityVersion issue, forcing upgrade from base version"
  398. echo " This will have for effect to reload the current version and set the featureCompatibilityVersion" >&2
  399. upgrade_path=("$current_image_version" "$image_version" "${upgrade_path[@]}")
  400. break
  401. else
  402. err "Already tried to mitigate featureCompatibilityVersion without success..."
  403. break 2
  404. fi
  405. fi
  406. err "Upgrade to ${WHITE}$image_version${NORMAL} ${DARKRED}failed${NORMAL}"
  407. failed=1
  408. break 2
  409. done
  410. done
  411. if [ -z "$unexpected_stop" ] && [ -z "$expected_stop" ]; then
  412. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  413. if [ -n "$migration_container" ]; then
  414. err "Stopping migration docker container '$migration_container' after failure"
  415. docker stop "$migration_container"
  416. fi
  417. fi
  418. if [ -n "$failed" ]; then
  419. if [ -e "$MIGRATION_TMPDIR/dump-${current_image_version}.log" ]; then
  420. echo "${WHITE}Failing dump logs:${NORMAL}" >&2
  421. cat "$MIGRATION_TMPDIR/dump-${current_image_version}.log" | prefix " ${GRAY}|${NORMAL} " >&2
  422. fi
  423. if [ -e "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log" ]; then
  424. echo "${WHITE}Failing migration logs:${NORMAL}" >&2
  425. cat "$MIGRATION_TMPDIR/migration-${current_image_version}_${image_version}.log" | prefix " ${GRAY}|${NORMAL} " >&2
  426. fi
  427. fi
  428. if [ -n "$unexpected_stop" ]; then
  429. if [ -e "$SERVICE_DATASTORE.migration-${current_image_version}-${uuid}" ]; then
  430. if [ -e "$SERVICE_DATASTORE" ]; then
  431. mv -v "$SERVICE_DATASTORE"{,".migration-${image_version}-${uuid}-failed"} || {
  432. err "Couldn't move back datastore to backup location for $SERVICE_NAME"
  433. }
  434. fi
  435. mv -v "$SERVICE_DATASTORE"{".migration-${current_image_version}-${uuid}",} || {
  436. err "Couldn't move back datastore to backup location for $SERVICE_NAME"
  437. }
  438. fi
  439. err "Migration failed unexpectedly."
  440. exit 1
  441. fi
  442. if [ "$starting_image_version" == "$current_image_version" ]; then
  443. warn "Database not migrated. Current version is still: ${WHITE}$current_image_version${NORMAL}" >&2
  444. exit 1
  445. fi
  446. (
  447. . "$CHARM_PATH/lib/common"
  448. cp "$MIGRATION_TMPDIR/pgpass" "$LOCAL_DB_PASSFILE"
  449. ) || {
  450. err "Couldn't restore postgres password file."
  451. exit 1
  452. }
  453. for migration_backup_dir in "${migration_backup_dirs[@]}"; do
  454. out=$(rm -rf "$migration_backup_dir") || {
  455. warn "Couldn't remove backup directory $migration_backup_dir"
  456. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  457. }
  458. done
  459. ## This next line has to be on stdout, it is used by `vps` to get information
  460. info "${GREEN}Successfully${NORMAL} upgraded from ${WHITE}$starting_image_version${NORMAL} to ${WHITE}$current_image_version${NORMAL}" 2>&1
  461. if [ -z "$no_hint" ]; then
  462. cat <<EOF >&2
  463. Don't forget to force the version in your \`\`compose.yml\`\`. For instance:
  464. ${DARKYELLOW}$SERVICE_NAME${NORMAL}:
  465. ${DARKGRAY}# ...${NORMAL}
  466. ${WHITE}docker-compose${NORMAL}:
  467. ${WHITE}image${NORMAL}: docker.0k.io/postgres:${current_image_version}-myc
  468. ${DARKGRAY}# ...${NORMAL}
  469. You could do this automatically with:
  470. yq eval -i ".$SERVICE_NAME.docker-compose.image = \"docker.0k.io/postgres:${current_image_version}-myc\" // \"\"" compose.yml
  471. And don't forget to run \`compose up\` afterwards.
  472. EOF
  473. fi
  474. exit 0