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.

396 lines
13 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. mongo: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 mongod's version from image's name $image"
  55. image_version="${image#*:}"
  56. image_version="${image_version%-myc}"
  57. else
  58. debug "Looking for mongod's version in image $image"
  59. if ! out=$(docker run --rm -i --entrypoint mongod "$image" --version); then
  60. err "Couldn't infer image '$image' mongod's version."
  61. exit 1
  62. fi
  63. out=${out%%$'\n'*}
  64. out=${out%%$'\r'*}
  65. image_version=${out#db version v}
  66. fi
  67. echo "$image_version"
  68. }
  69. mongo:container:version() {
  70. local container="$1" version
  71. if ! out=$(docker exec "$container" mongod --version); then
  72. err "Couldn't infer container's '$container' mongod's version."
  73. return 1
  74. fi
  75. out=${out%%$'\n'*}
  76. out=${out%%$'\r'*}
  77. version=${out#db version v}
  78. echo "$version"
  79. }
  80. mongo:container:mongo () {
  81. local container="$1"
  82. docker exec -i "$container" mongo 2>&1
  83. }
  84. mongo:container:fix_compat() {
  85. local container="$1" image_version="$2" version
  86. if ! version=$(mongo:container:version "$container"); then
  87. err "Couldn't get container's mongod version"
  88. return 1
  89. fi
  90. ## No need of confirmation for version < 7.0.0
  91. if version_gt 7.0.0 "$version"; then
  92. mongo_cmd="db.adminCommand( { setFeatureCompatibilityVersion: \"${image_version%.*}\" } )"
  93. else
  94. mongo_cmd="db.adminCommand( { setFeatureCompatibilityVersion: \"${image_version%.*}\", confirm: true } )"
  95. fi
  96. out=$(echo "$mongo_cmd" | mongo:container:mongo "$container" 2>&1)
  97. if [[ "$out" == *"\"ok\" : 1"* ]] || [[ "$out" == *"ok: 1"* ]]; then
  98. debug "Feature Compatibility set to ${WHITE}${image_version%.*}${NORMAL}"
  99. else
  100. err "Failed to set feature compatibility version to ${WHITE}${image_version%.*}${NORMAL}:"
  101. printf "%s" "$out" | prefix " | " >&2
  102. return 1
  103. fi
  104. }
  105. mongo:container:enable_read() {
  106. local container="$1"
  107. ## Enable read if db version >= 4.2
  108. if ! version=$(mongo:container:version "$container"); then
  109. err "Couldn't get container's mongod version"
  110. exit 1
  111. fi
  112. if version_gt "$version" 4.1; then
  113. cmd="db.getMongo().setSecondaryOk()"
  114. out=$(mongo:container:mongo "$container" <<<"$cmd") || {
  115. err "Failed database command. Output:"
  116. echo "$out" | prefix " | " >&2
  117. exit 1
  118. }
  119. fi
  120. }
  121. current_image_version=$(mongo:image:version "${DOCKER_BASE_IMAGE}") || exit 1
  122. if ! [[ "$current_image_version" =~ ^[0-9]+.[0-9]+.[0-9]+$ ]]; then
  123. err "Current mongo version ${WHITE}$current_image_version${NORMAL} is unsupported yet."
  124. exit 1
  125. fi
  126. starting_image_version=$current_image_version
  127. ## This next line has to be on stdout, it is used by `vps` to get information
  128. info "Current mongo version: ${WHITE}$current_image_version${NORMAL}" 2>&1
  129. last_available_versions=(
  130. $(DEBUG= docker:tags:fetch docker.0k.io/mongo 30 '[0-9]+\.[0-9+]\.[0-9]+-myc$' |
  131. sed -r 's/-myc$//g' |
  132. sort -rV)
  133. )
  134. debug "Last available versions: ${WHITE}${last_available_versions[*]}${NORMAL}"
  135. if [ -z "$target" ]; then
  136. info "Latest version available: ${WHITE}${last_available_versions[0]}${NORMAL}"
  137. fi
  138. ## XXXvlab: put this in kal-shlib-common
  139. version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
  140. last_upgradable_versions=()
  141. for v in "${last_available_versions[@]}"; do
  142. minor="${v#*.}"
  143. minor="${minor%.**}"
  144. ## In mongo versioning system, only even minor number are stable
  145. (( "$minor" % 2 )) && continue
  146. if version_gt "$v" "$current_image_version"; then
  147. last_upgradable_versions+=("$v")
  148. fi
  149. done
  150. debug "Last upgradable versions: ${WHITE}${last_upgradable_versions[*]}${NORMAL}"
  151. if [ "${#last_upgradable_versions[@]}" == 0 ]; then
  152. if [ -n "$target" ] && [ "$target" != "$current_image_version" ]; then
  153. warn "Provided target version ${WHITE}$target${NORMAL} is likely incorrect."
  154. echo " It is either non-existent and/or inferior to current version." >&2
  155. fi
  156. ## This next line has to be on stdout, it is used by `vps` to get information
  157. info "${DARKYELLOW}mongo${NORMAL} is already ${GREEN}up-to-date${NORMAL}." 2>&1
  158. exit 0
  159. fi
  160. if [ -z "$target" ]; then
  161. info "Target latest version: ${WHITE}${last_upgradable_versions[0]}${NORMAL}"
  162. target=${last_upgradable_versions[0]}
  163. else
  164. if [[ " ${last_available_versions[*]} " != *" $target "* ]]; then
  165. err "Invalid version $target selected, please specify one of:"
  166. for v in "${last_upgradable_versions[@]}"; do
  167. echo " - $v"
  168. done >&2
  169. exit 1
  170. fi
  171. last_upgradable_versions_filtered=()
  172. info "Target version ${WHITE}$target${NORMAL}"
  173. for v in "${last_upgradable_versions[@]}"; do
  174. if [ "$target" == "$v" ] || version_gt "$target" "$v"; then
  175. last_upgradable_versions_filtered+=("$v")
  176. fi
  177. done
  178. last_upgradable_versions=("${last_upgradable_versions_filtered[@]}")
  179. fi
  180. upgrade_path=($(printf "%s\n" "${last_upgradable_versions[@]}" | sort -rV | awk -F '.' '!seen[$1 "." $2]++ {print}' | sort -V))
  181. containers="$(get_running_containers_for_service "$SERVICE_NAME")"
  182. debug "Running containers for service $SERVICE_NAME: $containers"
  183. debug "Upgrade path: ${WHITE}${upgrade_path[@]}${NORMAL}"
  184. container_stopped=()
  185. if [ -n "$containers" ]; then
  186. #err "Running container(s) for $DARKYELLOW$SERVICE_NAME$NORMAL are still running:"
  187. for container in $containers; do
  188. debug "Stopping container $container"
  189. docker:container:stop "$container" >/dev/null || {
  190. err "Failed to stop container '$container'."
  191. exit 1
  192. }
  193. docker rm "$container" > /dev/null
  194. ## Check that the container is actually removed
  195. if [ -n "$(docker ps -q -f name="$container")" ]; then
  196. err "Failed to remove container '$container'."
  197. exit 1
  198. fi
  199. container_stopped+=("$container")
  200. done
  201. fi
  202. ## XXXvlab: taking first container is probably not a good idea
  203. container="$(echo "$containers" | head -n 1)"
  204. settmpdir MIGRATION_TMPDIR
  205. set -o pipefail
  206. failed=
  207. while [[ "${#upgrade_path[@]}" != 0 ]]; do
  208. image_version="${upgrade_path[0]}"
  209. upgrade_path=("${upgrade_path[@]:1}")
  210. ## Prevent build of image instead of pulling it
  211. next_image="docker.0k.io/mongo:${image_version}-myc"
  212. if ! docker_has_image "$next_image"; then
  213. out=$(docker pull "$next_image" 2>&1) || {
  214. err "Couldn't pull image $next_image:"
  215. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} " >&2
  216. unexpected_stop=1
  217. break 1
  218. }
  219. fi
  220. while true; do
  221. info "Upgrading step ${WHITE}$current_image_version${NORMAL} => ${WHITE}$image_version${NORMAL}"
  222. rm -f "$MIGRATION_TMPDIR/migration.log" && touch "$MIGRATION_TMPDIR/migration.log"
  223. uuid=$(openssl rand -hex 16)
  224. (
  225. {
  226. cmd=(
  227. compose --no-hooks
  228. --add-compose-content="$SERVICE_NAME:
  229. docker-compose:
  230. image: docker.0k.io/mongo:${image_version}-myc
  231. hostname: $SERVICE_NAME
  232. "
  233. run --rm --label "migration-uuid=$uuid" "$SERVICE_NAME"
  234. )
  235. echo COMMAND: "${cmd[@]}"
  236. "${cmd[@]}" 2>&1
  237. echo "ERRORLEVEL: $?"
  238. } | tee -a "$MIGRATION_TMPDIR/migration.log" >/dev/null
  239. ) &
  240. pid="$!"
  241. debug "Migration launched with PID ${YELLOW}$pid${NORMAL}"
  242. expected_stop=""
  243. unexpected_stop=""
  244. errlvl=""
  245. while read -r line; do
  246. case "$line" in
  247. *" NETWORK "*"[initandlisten] waiting for connections on port"*|\
  248. *" NETWORK "*"[listener] waiting for connections on port"*|\
  249. *"\"NETWORK\""*"\"Waiting for connections\""*)
  250. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  251. if [ "${image_version%.*}" != "${current_image_version%.*}" ]; then
  252. mongo:container:enable_read "$migration_container" &&
  253. mongo:container:fix_compat "$migration_container" "$image_version" || {
  254. failed=1
  255. break 3
  256. }
  257. fi
  258. current_image="$next_image"
  259. current_image_version="$image_version"
  260. out=$(docker stop "$migration_container" 2>&1) || {
  261. err "Failed to stop the migration container $migration_container for $image_version:"
  262. printf "%s\n" "$out" | prefix " ${GRAY}|${NORMAL} "
  263. break 3
  264. }
  265. expected_stop="1"
  266. debug "Interrupting server process after migration (${YELLOW}$pid${NORMAL})" >&2
  267. kill "$pid"
  268. errlvl="0"
  269. break
  270. ;;
  271. *"getaddrinfo(\"$SERVICE_NAME\") failed: Name or service not known"*)
  272. err "Mongo issue with resolving '$SERVICE_NAME'"
  273. failed=1
  274. break 3
  275. ;;
  276. *"Replication has not yet been configured"*)
  277. warn "Mongo was not setup right away, could lead to issues"
  278. ;;
  279. "ERRORLEVEL: "*)
  280. unexpected_stop="1"
  281. errlvl="${line#ERRORLEVEL: }"
  282. err "Unexpected stop of mongod migration container with errorlevel $errlvl"
  283. break
  284. ;;
  285. esac
  286. done < <(tail -f "$MIGRATION_TMPDIR/migration.log")
  287. [ "$expected_stop" == "1" ] && {
  288. docker rmi "$current_image" >/dev/null 2>&1 || true
  289. continue 2
  290. }
  291. ##
  292. ## Damage control, there are some things we can solve
  293. ##
  294. if grep "UPGRADE PROBLEM: Found an invalid featureCompatibilityVersion document" "$MIGRATION_TMPDIR/migration.log" >/dev/null 2>&1; then
  295. if [ -z "$feature_comp" ]; then
  296. feature_comp=1
  297. info "Detected featureCompatibilityVersion issue, forcing upgrade from base version"
  298. echo " This will have for effect to reload the current version and set the featureCompatibilityVersion" >&2
  299. upgrade_path=("$current_image_version" "$image_version" "${upgrade_path[@]}")
  300. break
  301. else
  302. err "Already tried to mitigate featureCompatibilityVersion without success..."
  303. break 2
  304. fi
  305. fi
  306. err "Upgrade to ${WHITE}$image_version${NORMAL} ${DARKRED}failed${NORMAL}"
  307. failed=1
  308. break 2
  309. done
  310. done
  311. if [ -z "$unexpected_stop" -a -z "$expected_stop" ]; then
  312. migration_container=$(docker ps --filter "label=migration-uuid=$uuid" --format '{{.ID}}')
  313. err "Stopping migration docker container '$migration_container' after failure"
  314. docker stop "$migration_container"
  315. fi
  316. if [ -n "$failed" ]; then
  317. echo "${WHITE}Failing migration logs:${NORMAL}" >&2
  318. cat "$MIGRATION_TMPDIR/migration.log" | prefix " ${GRAY}|${NORMAL} " >&2
  319. fi
  320. if [ "$starting_image_version" == "$current_image_version" ]; then
  321. warn "Database not migrated. Current version is still: ${WHITE}$current_image_version${NORMAL}" >&2
  322. exit 1
  323. fi
  324. ## This next line has to be on stdout, it is used by `vps` to get information
  325. info "${GREEN}Successfully${NORMAL} upgraded from ${WHITE}$starting_image_version${NORMAL} to ${WHITE}$current_image_version${NORMAL}" 2>&1
  326. if [ -z "$no_hint" ]; then
  327. cat <<EOF >&2
  328. Don't forget to force the version in your \`\`compose.yml\`\`. For instance:
  329. ${DARKYELLOW}$SERVICE_NAME${NORMAL}:
  330. ${DARKGRAY}# ...${NORMAL}
  331. ${WHITE}docker-compose${NORMAL}:
  332. ${WHITE}image${NORMAL}: docker.0k.io/mongo:${current_image_version}-myc
  333. ${DARKGRAY}# ...${NORMAL}
  334. You could do this automatically with:
  335. yq eval -i ".$SERVICE_NAME.docker-compose.image = \"docker.0k.io/mongo:${current_image_version}-myc\" // \"\"" compose.yml
  336. And don't forget to run \`compose up\` afterwards.
  337. EOF
  338. fi
  339. exit 0