bashbrew.sh 10.0 KB

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