123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- #!/bin/bash
- set -e
- # make sure we can GTFO
- trap 'echo >&2 Ctrl+C captured, exiting; exit 1' SIGINT
- dir="$(dirname "$(readlink -f "$BASH_SOURCE")")"
- library="$(dirname "$dir")/library"
- src="$dir/src"
- logs="$dir/logs"
- namespaces='_'
- docker='docker'
- retries='4'
- self="$(basename "$0")"
- usage() {
- cat <<-EOUSAGE
-
- usage: $self [build|push|list] [options] [repo[:tag] ...]
- ie: $self build --all
- $self push debian ubuntu:12.04
- $self list --namespaces='_' debian:7 hello-world
-
- This script processes the specified Docker images using the corresponding
- repository manifest files.
-
- common options:
- --all Build all repositories specified in library
- --docker="$docker"
- Use a custom Docker binary
- --retries="$retries"
- How many times to try again if the build/push fails before
- considering it a lost cause (always attempts a minimum of
- one time, but maximum of one plus this number)
- --help, -h, -? Print this help message
- --library="$library"
- Where to find repository manifest files
- --logs="$logs"
- Where to store the build logs
- --namespaces="$namespaces"
- Space separated list of image namespaces to act upon
-
- Note that "_" is a special case here for the unprefixed
- namespace (ie, "debian" vs "library/debian"), and as such
- will be implicitly ignored by the "push" subcommand
-
- Also note that "build" will always tag to the unprefixed
- namespace because it is necessary to do so for dependent
- images to use FROM correctly (think "onbuild" variants that
- are "FROM base-image:some-version")
- --uniq
- Only process the first tag of identical images
- This is not recommended for build or push
- i.e. process python:2.7, but not python:2
-
- build options:
- --no-build Don't build, print what would build
- --no-clone Don't pull/clone Git repositories
- --src="$src"
- Where to store cloned Git repositories (GOPATH style)
-
- push options:
- --no-push Don't push, print what would push
-
- EOUSAGE
- }
- # arg handling
- opts="$(getopt -o 'h?' --long 'all,docker:,help,library:,logs:,namespaces:,no-build,no-clone,no-push,retries:,src:,uniq' -- "$@" || { usage >&2 && false; })"
- eval set -- "$opts"
- doClone=1
- doBuild=1
- doPush=1
- buildAll=
- onlyUniq=
- while true; do
- flag="$1"
- shift
- case "$flag" in
- --all) buildAll=1 ;;
- --docker) docker="$1" && shift ;;
- --help|-h|'-?') usage && exit 0 ;;
- --library) library="$1" && shift ;;
- --logs) logs="$1" && shift ;;
- --namespaces) namespaces="$1" && shift ;;
- --no-build) doBuild= ;;
- --no-clone) doClone= ;;
- --no-push) doPush= ;;
- --retries) retries="$1" && (( retries++ )) && shift ;;
- --src) src="$1" && shift ;;
- --uniq) onlyUniq=1 ;;
- --) break ;;
- *)
- {
- echo "error: unknown flag: $flag"
- usage
- } >&2
- exit 1
- ;;
- esac
- done
- if [ ! -d "$library" ]; then
- echo >&2 "error: library directory '$library' does not exist"
- exit 1
- fi
- library="$(readlink -f "$library")"
- mkdir -p "$src" "$logs"
- src="$(readlink -f "$src")"
- logs="$(readlink -f "$logs")"
- # which subcommand
- subcommand="$1"
- shift || { usage >&2 && exit 1; }
- case "$subcommand" in
- build|push|list) ;;
- *)
- {
- echo "error: unknown subcommand: $1"
- usage
- } >&2
- exit 1
- ;;
- esac
- repos=()
- if [ "$buildAll" ]; then
- repos=( "$library"/* )
- fi
- repos+=( "$@" )
- repos=( "${repos[@]%/}" )
- if [ "${#repos[@]}" -eq 0 ]; then
- {
- echo 'error: no repos specified'
- usage
- } >&2
- exit 1
- fi
- # globals for handling the repo queue and repo info parsed from library
- queue=()
- declare -A repoGitRepo=()
- declare -A repoGitRef=()
- declare -A repoGitDir=()
- declare -A repoUniq=()
- logDir="$logs/$subcommand-$(date +'%Y-%m-%d--%H-%M-%S')"
- mkdir -p "$logDir"
- latestLogDir="$logs/latest" # this gets shiny symlinks to the latest buildlog for each repo we've seen since the creation of the logs dir
- mkdir -p "$latestLogDir"
- didFail=
- # gather all the `repo:tag` combos to build
- for repoTag in "${repos[@]}"; do
- repo="${repoTag%%:*}"
- tag="${repoTag#*:}"
- [ "$repo" != "$tag" ] || tag=
-
- if [ "$repo" = 'http' -o "$repo" = 'https' ] && [[ "$tag" == //* ]]; then
- # IT'S A URL!
- repoUrl="$repo:${tag%:*}"
- repo="$(basename "$repoUrl")"
- if [ "${tag##*:}" != "$tag" ]; then
- tag="${tag##*:}"
- else
- tag=
- fi
- repoTag="${repo}${tag:+:$tag}"
-
- echo "$repoTag ($repoUrl)" >> "$logDir/repos.txt"
-
- cmd=( curl -sSL --compressed "$repoUrl" )
- else
- if [ -f "$repo" ]; then
- repoFile="$repo"
- repo="$(basename "$repoFile")"
- repoTag="${repo}${tag:+:$tag}"
- else
- repoFile="$library/$repo"
- fi
-
- repoFile="$(readlink -f "$repoFile")"
- echo "$repoTag ($repoFile)" >> "$logDir/repos.txt"
-
- cmd=( cat "$repoFile" )
- fi
-
- if [ "${repoGitRepo[$repoTag]}" ]; then
- if [ "$onlyUniq" ]; then
- uniqLine="${repoGitRepo[$repoTag]}@${repoGitRef[$repoTag]} ${repoGitDir[$repoTag]}"
- if [ -z "${repoUniq[$uniqLine]}" ]; then
- queue+=( "$repoTag" )
- repoUniq[$uniqLine]=$repoTag
- fi
- else
- queue+=( "$repoTag" )
- fi
- continue
- fi
-
- if ! manifest="$("${cmd[@]}")"; then
- echo >&2 "error: failed to fetch $repoTag (${cmd[*]})"
- exit 1
- fi
-
- # parse the repo manifest file
- IFS=$'\n'
- repoTagLines=( $(echo "$manifest" | grep -vE '^#|^\s*$') )
- unset IFS
-
- tags=()
- for line in "${repoTagLines[@]}"; do
- tag="$(echo "$line" | awk -F ': +' '{ print $1 }')"
- for parsedRepoTag in "${tags[@]}"; do
- if [ "$repo:$tag" = "$parsedRepoTag" ]; then
- echo >&2 "error: tag '$tag' is duplicated in '${cmd[@]}'"
- exit 1
- fi
- done
-
- repoDir="$(echo "$line" | awk -F ': +' '{ print $2 }')"
-
- gitUrl="${repoDir%%@*}"
- commitDir="${repoDir#*@}"
- gitRef="${commitDir%% *}"
- gitDir="${commitDir#* }"
- if [ "$gitDir" = "$commitDir" ]; then
- gitDir=
- fi
-
- gitRepo="${gitUrl#*://}"
- gitRepo="${gitRepo%/}"
- gitRepo="${gitRepo%.git}"
- gitRepo="${gitRepo%/}"
- gitRepo="$src/$gitRepo"
-
- if [ "$subcommand" = 'build' ]; then
- if [ -z "$doClone" ]; then
- if [ "$doBuild" -a ! -d "$gitRepo" ]; then
- echo >&2 "error: directory not found: $gitRepo"
- exit 1
- fi
- else
- if [ ! -d "$gitRepo" ]; then
- mkdir -p "$(dirname "$gitRepo")"
- echo "Cloning $repo ($gitUrl) ..."
- git clone -q "$gitUrl" "$gitRepo"
- else
- # if we don't have the "ref" specified, "git fetch" in the hopes that we get it
- if ! ( cd "$gitRepo" && git rev-parse --verify "${gitRef}^{commit}" &> /dev/null ); then
- echo "Fetching $repo ($gitUrl) ..."
- ( cd "$gitRepo" && git fetch -q --all && git fetch -q --tags )
- fi
- fi
-
- # disable any automatic garbage collection too, just to help make sure we keep our dangling commit objects
- ( cd "$gitRepo" && git config gc.auto 0 )
- fi
- fi
-
- repoGitRepo[$repo:$tag]="$gitRepo"
- repoGitRef[$repo:$tag]="$gitRef"
- repoGitDir[$repo:$tag]="$gitDir"
- tags+=( "$repo:$tag" )
- done
-
- if [ "$repo" != "$repoTag" ]; then
- tags=( "$repoTag" )
- fi
-
- if [ "$onlyUniq" ]; then
- for rt in "${tags[@]}"; do
- uniqLine="${repoGitRepo[$rt]}@${repoGitRef[$rt]} ${repoGitDir[$rt]}"
- if [ -z "${repoUniq[$uniqLine]}" ]; then
- queue+=( "$rt" )
- repoUniq[$uniqLine]=$rt
- fi
- done
- else
- # add all tags we just parsed
- queue+=( "${tags[@]}" )
- fi
- done
- # usage: gitCheckout "$gitRepo" "$gitRef" "$gitDir"
- gitCheckout() {
- [ "$1" -a "$2" ] || return 1 # "$3" is allowed to be the empty string
- (
- set -x
- cd "$1"
- git reset -q HEAD
- git checkout -q -- .
- git clean -dfxq
- git checkout -q "$2" --
- cd "$1/$3"
- "$dir/git-set-mtimes"
- )
- return 0
- }
- set -- "${queue[@]}"
- while [ "$#" -gt 0 ]; do
- repoTag="$1"
- gitRepo="${repoGitRepo[$repoTag]}"
- gitRef="${repoGitRef[$repoTag]}"
- gitDir="${repoGitDir[$repoTag]}"
- shift
- if [ -z "$gitRepo" ]; then
- echo >&2 'Unknown repo:tag:' "$repoTag"
- didFail=1
- continue
- fi
-
- thisLog="$logDir/$subcommand-$repoTag.log"
- touch "$thisLog"
- thisLogSymlink="$latestLogDir/$(basename "$thisLog")"
- ln -sf "$thisLog" "$thisLogSymlink"
-
- case "$subcommand" in
- build)
- echo "Processing $repoTag ..."
-
- if ! ( cd "$gitRepo" && git rev-parse --verify "${gitRef}^{commit}" &> /dev/null ); then
- echo "- failed; invalid ref: $gitRef"
- didFail=1
- continue
- fi
-
- dockerfilePath="$gitDir/Dockerfile"
- dockerfilePath="${dockerfilePath#/}" # strip leading "/" (for when gitDir is '') because "git show" doesn't like it
-
- if ! dockerfile="$(cd "$gitRepo" && git show "$gitRef":"$dockerfilePath")"; then
- echo "- failed; missing '$dockerfilePath' at '$gitRef' ?"
- didFail=1
- continue
- fi
-
- IFS=$'\n'
- froms=( $(echo "$dockerfile" | awk 'toupper($1) == "FROM" { print $2 ~ /:/ ? $2 : $2":latest" }') )
- unset IFS
-
- for from in "${froms[@]}"; do
- for queuedRepoTag in "$@"; do
- if [ "$from" = "$queuedRepoTag" ]; then
- # 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
- echo "- deferred; FROM $from"
- set -- "$@" "$repoTag"
- continue 3
- fi
- done
- done
-
- if [ "$doBuild" ]; then
- if ! gitCheckout "$gitRepo" "$gitRef" "$gitDir" &>> "$thisLog"; then
- echo "- failed 'git checkout'; see $thisLog"
- didFail=1
- continue
- fi
-
- tries="$retries"
- while ! ( set -x && "$docker" build -t "$repoTag" "$gitRepo/$gitDir" ) &>> "$thisLog"; do
- (( tries-- )) || true
- if [ $tries -le 0 ]; then
- echo >&2 "- failed 'docker build'; see $thisLog"
- didFail=1
- continue 2
- fi
- done
-
- for namespace in $namespaces; do
- if [ "$namespace" = '_' ]; then
- # images FROM other images is explicitly supported
- continue
- fi
- if ! ( set -x && "$docker" tag -f "$repoTag" "$namespace/$repoTag" ) &>> "$thisLog"; then
- echo "- failed 'docker tag'; see $thisLog"
- didFail=1
- continue
- fi
- done
- fi
- ;;
- list)
- for namespace in $namespaces; do
- if [ "$namespace" = '_' ]; then
- echo "$repoTag"
- else
- echo "$namespace/$repoTag"
- fi
- done
- ;;
- push)
- for namespace in $namespaces; do
- if [ "$namespace" = '_' ]; then
- # can't "docker push debian"; skip this namespace
- continue
- fi
- if [ "$doPush" ]; then
- echo "Pushing $namespace/$repoTag..."
- tries="$retries"
- while ! ( set -x && "$docker" push "$namespace/$repoTag" < /dev/null ) &>> "$thisLog"; do
- (( tries-- )) || true
- if [ $tries -le 0 ]; then
- echo >&2 "- $namespace/$repoTag failed to push; see $thisLog"
- continue 2
- fi
- done
- else
- echo "$docker push" "$namespace/$repoTag"
- fi
- done
- ;;
- esac
-
- if [ ! -s "$thisLog" ]; then
- rm "$thisLog" "$thisLogSymlink"
- fi
- done
- [ -z "$didFail" ]
|