bashbrew.sh 9.2 KB

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