bashbrew.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #!/bin/bash
  2. set -e
  3. # make sure we can GTFO
  4. trap 'echo >&2 Ctrl+C captured, exiting; exit 1' SIGINT
  5. dir="$(dirname "$(readlink -f "$BASH_SOURCE")")"
  6. library="$dir/../library"
  7. src="$dir/src"
  8. logs="$dir/logs"
  9. namespaces='_'
  10. docker='docker'
  11. retries='4'
  12. library="$(readlink -f "$library")"
  13. src="$(readlink -f "$src")"
  14. logs="$(readlink -f "$logs")"
  15. self="$(basename "$0")"
  16. usage() {
  17. cat <<-EOUSAGE
  18. usage: $self [build|push|list] [options] [repo[:tag] ...]
  19. ie: $self build --all
  20. $self push debian ubuntu:12.04
  21. $self list --namespaces='_' debian:7 hello-world
  22. This script processes the specified Docker images using the corresponding
  23. repository manifest files.
  24. common options:
  25. --all Build all repositories specified in library
  26. --docker="$docker"
  27. Use a custom Docker binary
  28. --retries="$retries"
  29. How many times to try again if the build/push fails before
  30. considering it a lost cause (always attempts a minimum of
  31. one time, but maximum of one plus this number)
  32. --help, -h, -? Print this help message
  33. --library="$library"
  34. Where to find repository manifest files
  35. --logs="$logs"
  36. Where to store the build logs
  37. --namespaces="$namespaces"
  38. Space separated list of image namespaces to act upon
  39. Note that "_" is a special case here for the unprefixed
  40. namespace (ie, "debian" vs "library/debian"), and as such
  41. will be implicitly ignored by the "push" subcommand
  42. Also note that "build" will always tag to the unprefixed
  43. namespace because it is necessary to do so for dependent
  44. images to use FROM correctly (think "onbuild" variants that
  45. are "FROM base-image:some-version")
  46. --uniq
  47. Only process the first tag of identical images
  48. This is not recommended for build or push
  49. i.e. process python:2.7, but not python:2
  50. build options:
  51. --no-build Don't build, print what would build
  52. --no-clone Don't pull/clone Git repositories
  53. --src="$src"
  54. Where to store cloned Git repositories (GOPATH style)
  55. push options:
  56. --no-push Don't push, print what would push
  57. EOUSAGE
  58. }
  59. # arg handling
  60. opts="$(getopt -o 'h?' --long 'all,docker:,help,library:,logs:,namespaces:,no-build,no-clone,no-push,retries:,src:,uniq' -- "$@" || { usage >&2 && false; })"
  61. eval set -- "$opts"
  62. doClone=1
  63. doBuild=1
  64. doPush=1
  65. buildAll=
  66. onlyUniq=
  67. while true; do
  68. flag="$1"
  69. shift
  70. case "$flag" in
  71. --all) buildAll=1 ;;
  72. --docker) docker="$1" && shift ;;
  73. --help|-h|'-?') usage && exit 0 ;;
  74. --library) library="$1" && shift ;;
  75. --logs) logs="$1" && shift ;;
  76. --namespaces) namespaces="$1" && shift ;;
  77. --no-build) doBuild= ;;
  78. --no-clone) doClone= ;;
  79. --no-push) doPush= ;;
  80. --retries) retries="$1" && (( retries++ )) && shift ;;
  81. --src) src="$1" && shift ;;
  82. --uniq) onlyUniq=1 ;;
  83. --) break ;;
  84. *)
  85. {
  86. echo "error: unknown flag: $flag"
  87. usage
  88. } >&2
  89. exit 1
  90. ;;
  91. esac
  92. done
  93. # which subcommand
  94. subcommand="$1"
  95. shift || { usage >&2 && exit 1; }
  96. case "$subcommand" in
  97. build|push|list) ;;
  98. *)
  99. {
  100. echo "error: unknown subcommand: $1"
  101. usage
  102. } >&2
  103. exit 1
  104. ;;
  105. esac
  106. repos=()
  107. if [ "$buildAll" ]; then
  108. repos=( "$library"/* )
  109. fi
  110. repos+=( "$@" )
  111. repos=( "${repos[@]%/}" )
  112. if [ "${#repos[@]}" -eq 0 ]; then
  113. {
  114. echo 'error: no repos specified'
  115. usage
  116. } >&2
  117. exit 1
  118. fi
  119. # globals for handling the repo queue and repo info parsed from library
  120. queue=()
  121. declare -A repoGitRepo=()
  122. declare -A repoGitRef=()
  123. declare -A repoGitDir=()
  124. declare -A repoUniq=()
  125. logDir="$logs/$subcommand-$(date +'%Y-%m-%d--%H-%M-%S')"
  126. mkdir -p "$logDir"
  127. latestLogDir="$logs/latest" # this gets shiny symlinks to the latest buildlog for each repo we've seen since the creation of the logs dir
  128. mkdir -p "$latestLogDir"
  129. didFail=
  130. # gather all the `repo:tag` combos to build
  131. for repoTag in "${repos[@]}"; do
  132. repo="${repoTag%%:*}"
  133. tag="${repoTag#*:}"
  134. [ "$repo" != "$tag" ] || tag=
  135. if [ "$repo" = 'http' -o "$repo" = 'https' ] && [[ "$tag" == //* ]]; then
  136. # IT'S A URL!
  137. repoUrl="$repo:${tag%:*}"
  138. repo="$(basename "$repoUrl")"
  139. if [ "${tag##*:}" != "$tag" ]; then
  140. tag="${tag##*:}"
  141. else
  142. tag=
  143. fi
  144. repoTag="${repo}${tag:+:$tag}"
  145. echo "$repoTag ($repoUrl)" >> "$logDir/repos.txt"
  146. cmd=( curl -sSL --compressed "$repoUrl" )
  147. else
  148. if [ -f "$repo" ]; then
  149. repoFile="$repo"
  150. repo="$(basename "$repoFile")"
  151. repoTag="${repo}${tag:+:$tag}"
  152. else
  153. repoFile="$library/$repo"
  154. fi
  155. repoFile="$(readlink -f "$repoFile")"
  156. echo "$repoTag ($repoFile)" >> "$logDir/repos.txt"
  157. cmd=( cat "$repoFile" )
  158. fi
  159. if [ "${repoGitRepo[$repoTag]}" ]; then
  160. if [ "$onlyUniq" ]; then
  161. uniqLine="${repoGitRepo[$repoTag]}@${repoGitRef[$repoTag]} ${repoGitDir[$repoTag]}"
  162. if [ -z "${repoUniq[$uniqLine]}" ]; then
  163. queue+=( "$repoTag" )
  164. repoUniq[$uniqLine]=$repoTag
  165. fi
  166. else
  167. queue+=( "$repoTag" )
  168. fi
  169. continue
  170. fi
  171. if ! manifest="$("${cmd[@]}")"; then
  172. echo >&2 "error: failed to fetch $repoTag (${cmd[*]})"
  173. exit 1
  174. fi
  175. # parse the repo manifest file
  176. IFS=$'\n'
  177. repoTagLines=( $(echo "$manifest" | grep -vE '^#|^\s*$') )
  178. unset IFS
  179. tags=()
  180. for line in "${repoTagLines[@]}"; do
  181. tag="$(echo "$line" | awk -F ': +' '{ print $1 }')"
  182. for parsedRepoTag in "${tags[@]}"; do
  183. if [ "$repo:$tag" = "$parsedRepoTag" ]; then
  184. echo >&2 "error: tag '$tag' is duplicated in '${cmd[@]}'"
  185. exit 1
  186. fi
  187. done
  188. repoDir="$(echo "$line" | awk -F ': +' '{ print $2 }')"
  189. gitUrl="${repoDir%%@*}"
  190. commitDir="${repoDir#*@}"
  191. gitRef="${commitDir%% *}"
  192. gitDir="${commitDir#* }"
  193. if [ "$gitDir" = "$commitDir" ]; then
  194. gitDir=
  195. fi
  196. gitRepo="${gitUrl#*://}"
  197. gitRepo="${gitRepo%/}"
  198. gitRepo="${gitRepo%.git}"
  199. gitRepo="${gitRepo%/}"
  200. gitRepo="$src/$gitRepo"
  201. if [ "$subcommand" = 'build' ]; then
  202. if [ -z "$doClone" ]; then
  203. if [ "$doBuild" -a ! -d "$gitRepo" ]; then
  204. echo >&2 "error: directory not found: $gitRepo"
  205. exit 1
  206. fi
  207. else
  208. if [ ! -d "$gitRepo" ]; then
  209. mkdir -p "$(dirname "$gitRepo")"
  210. echo "Cloning $repo ($gitUrl) ..."
  211. git clone -q "$gitUrl" "$gitRepo"
  212. else
  213. # if we don't have the "ref" specified, "git fetch" in the hopes that we get it
  214. if ! ( cd "$gitRepo" && git rev-parse --verify "${gitRef}^{commit}" &> /dev/null ); then
  215. echo "Fetching $repo ($gitUrl) ..."
  216. ( cd "$gitRepo" && git fetch -q --all && git fetch -q --tags )
  217. fi
  218. fi
  219. # disable any automatic garbage collection too, just to help make sure we keep our dangling commit objects
  220. ( cd "$gitRepo" && git config gc.auto 0 )
  221. fi
  222. fi
  223. repoGitRepo[$repo:$tag]="$gitRepo"
  224. repoGitRef[$repo:$tag]="$gitRef"
  225. repoGitDir[$repo:$tag]="$gitDir"
  226. tags+=( "$repo:$tag" )
  227. done
  228. if [ "$repo" != "$repoTag" ]; then
  229. tags=( "$repoTag" )
  230. fi
  231. if [ "$onlyUniq" ]; then
  232. for rt in "${tags[@]}"; do
  233. uniqLine="${repoGitRepo[$rt]}@${repoGitRef[$rt]} ${repoGitDir[$rt]}"
  234. if [ -z "${repoUniq[$uniqLine]}" ]; then
  235. queue+=( "$rt" )
  236. repoUniq[$uniqLine]=$rt
  237. fi
  238. done
  239. else
  240. # add all tags we just parsed
  241. queue+=( "${tags[@]}" )
  242. fi
  243. done
  244. # usage: gitCheckout "$gitRepo" "$gitRef" "$gitDir"
  245. gitCheckout() {
  246. [ "$1" -a "$2" ] || return 1 # "$3" is allowed to be the empty string
  247. (
  248. set -x
  249. cd "$1"
  250. git reset -q HEAD
  251. git checkout -q -- .
  252. git clean -dfxq
  253. git checkout -q "$2" --
  254. cd "$1/$3"
  255. "$dir/git-set-mtimes"
  256. )
  257. return 0
  258. }
  259. set -- "${queue[@]}"
  260. while [ "$#" -gt 0 ]; do
  261. repoTag="$1"
  262. gitRepo="${repoGitRepo[$repoTag]}"
  263. gitRef="${repoGitRef[$repoTag]}"
  264. gitDir="${repoGitDir[$repoTag]}"
  265. shift
  266. if [ -z "$gitRepo" ]; then
  267. echo >&2 'Unknown repo:tag:' "$repoTag"
  268. didFail=1
  269. continue
  270. fi
  271. thisLog="$logDir/$subcommand-$repoTag.log"
  272. touch "$thisLog"
  273. thisLogSymlink="$latestLogDir/$(basename "$thisLog")"
  274. ln -sf "$thisLog" "$thisLogSymlink"
  275. case "$subcommand" in
  276. build)
  277. echo "Processing $repoTag ..."
  278. if ! ( cd "$gitRepo" && git rev-parse --verify "${gitRef}^{commit}" &> /dev/null ); then
  279. echo "- failed; invalid ref: $gitRef"
  280. didFail=1
  281. continue
  282. fi
  283. dockerfilePath="$gitDir/Dockerfile"
  284. dockerfilePath="${dockerfilePath#/}" # strip leading "/" (for when gitDir is '') because "git show" doesn't like it
  285. if ! dockerfile="$(cd "$gitRepo" && git show "$gitRef":"$dockerfilePath")"; then
  286. echo "- failed; missing '$dockerfilePath' at '$gitRef' ?"
  287. didFail=1
  288. continue
  289. fi
  290. IFS=$'\n'
  291. froms=( $(echo "$dockerfile" | awk 'toupper($1) == "FROM" { print $2 ~ /:/ ? $2 : $2":latest" }') )
  292. unset IFS
  293. for from in "${froms[@]}"; do
  294. for queuedRepoTag in "$@"; do
  295. if [ "$from" = "$queuedRepoTag" ]; then
  296. # a "FROM" in this image is being built later in our queue, so let's bail on this image for now and come back later
  297. echo "- deferred; FROM $from"
  298. set -- "$@" "$repoTag"
  299. continue 3
  300. fi
  301. done
  302. done
  303. if [ "$doBuild" ]; then
  304. if ! gitCheckout "$gitRepo" "$gitRef" "$gitDir" &>> "$thisLog"; then
  305. echo "- failed 'git checkout'; see $thisLog"
  306. didFail=1
  307. continue
  308. fi
  309. tries="$retries"
  310. while ! ( set -x && "$docker" build -t "$repoTag" "$gitRepo/$gitDir" ) &>> "$thisLog"; do
  311. (( tries-- )) || true
  312. if [ $tries -le 0 ]; then
  313. echo >&2 "- failed 'docker build'; see $thisLog"
  314. didFail=1
  315. continue 2
  316. fi
  317. done
  318. for namespace in $namespaces; do
  319. if [ "$namespace" = '_' ]; then
  320. # images FROM other images is explicitly supported
  321. continue
  322. fi
  323. if ! ( set -x && "$docker" tag -f "$repoTag" "$namespace/$repoTag" ) &>> "$thisLog"; then
  324. echo "- failed 'docker tag'; see $thisLog"
  325. didFail=1
  326. continue
  327. fi
  328. done
  329. fi
  330. ;;
  331. list)
  332. for namespace in $namespaces; do
  333. if [ "$namespace" = '_' ]; then
  334. echo "$repoTag"
  335. else
  336. echo "$namespace/$repoTag"
  337. fi
  338. done
  339. ;;
  340. push)
  341. for namespace in $namespaces; do
  342. if [ "$namespace" = '_' ]; then
  343. # can't "docker push debian"; skip this namespace
  344. continue
  345. fi
  346. if [ "$doPush" ]; then
  347. echo "Pushing $namespace/$repoTag..."
  348. tries="$retries"
  349. while ! ( set -x && "$docker" push "$namespace/$repoTag" < /dev/null ) &>> "$thisLog"; do
  350. (( tries-- )) || true
  351. if [ $tries -le 0 ]; then
  352. echo >&2 "- $namespace/$repoTag failed to push; see $thisLog"
  353. continue 2
  354. fi
  355. done
  356. else
  357. echo "$docker push" "$namespace/$repoTag"
  358. fi
  359. done
  360. ;;
  361. esac
  362. if [ ! -s "$thisLog" ]; then
  363. rm "$thisLog" "$thisLogSymlink"
  364. fi
  365. done
  366. [ -z "$didFail" ]