bashbrew.sh 11 KB

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