diff --git a/docker-artifact.sh b/docker-artifact.sh index ed91601..591b76c 100755 --- a/docker-artifact.sh +++ b/docker-artifact.sh @@ -2,62 +2,143 @@ set -e -verbose=1 -image="$1" -export searchpath="$2" -export tarfile="$(mktemp)-$image.tar" - -# check to see if image exists locally -if ! docker image inspect "$image" > /dev/null ; then - exit 1 -fi - -# Save image tar to temp file -[ $verbose ] && >&2 echo "Exporting image '$image' to temp file '$tarfile'..." -docker image save "$image" -o "$tarfile" - -# Set up cleanup to not leave image behind -function onexit() { - [ $verbose ] && >&2 echo "Cleaning up exported image '$tarfile'" - rm "$tarfile" +function docker_cli_plugin_metadata { + local vendor="infogulch" + local version="v0.0.1" + local description="Manage Artifacts in Docker Images" + local url="https://github.com/infogulch/docker-artifact" + printf '{"SchemaVersion":"0.1.0","Vendor":"%s","Version":"%s","ShortDescription":"%s","URL":"%s"}\n' \ + "$vendor" "$version" "$description" "$url" } -trap onexit EXIT -# Extract manifest and config file contents -[ $verbose ] && >&2 echo "Collecting metadata from image..." -manifest=$(tar -xf "$tarfile" -x manifest.json -O | jq) -config_file=$(echo "$manifest" | jq -r '.[0].Config') -config=$(tar -f "$tarfile" -x "$config_file" -O | jq) +__usage=" +Usage: docker artifact [command] [options] -# Combine manifest.json and config json to build a map from tar layer directory to layer id sha -export idmap=$(echo "$manifest" "$config" | jq -sr '[ [ .[0][0].Layers, .[1].rootfs.diff_ids ] | transpose[] | { (.[0]): .[1] } ] | reduce .[] as $x ({}; . * $x)') +Commands: -# Search each layer for a file matching $searchpath -# TODO also search for related whiteout files that start with `.wh.` -[ $verbose ] && >&2 echo "Searching layers for '$searchpath'..." -found=$(echo "$manifest" | jq '.[0].Layers[]' | xargs -I {} sh -c 'digest=$(echo "$idmap" | jq -r ".[\"{}\"]"); tar -f "$tarfile" -x {} -O | tar -t | sed s_^_/_ | grep -wx "$searchpath" | xargs -I [] echo "[]=$digest"') + ls - List files available to download directly from an image + download - Download files directly from an image that has been labeled + label - Label an image -foundcount=$(echo "$found" | grep -c . || true) +Command usage: -# If more than one is found, then bail -if [ $foundcount -gt 1 ]; then - >&2 echo "File was changed in multiple layers, aborting. Found files and layer ids:" - >&2 echo "$found" - exit 2 -elif [ $foundcount -eq 0 ]; then - >&2 echo "No files found matching '$searchpath'" - exit 3 -fi + docker artifact ls [options] image_name + docker artifact download [options] image_name file... + docker artifact label [options] image_name file... -labels=$(echo "$found" | sed 's_^.*$_--label "\0"_' | paste -d' ') +Options: + -v - verbose output + -q - quiet output -# Add a layer to the existing image to add the labels and tag the new image with the same image name -[ $verbose ] && >&2 echo "Rebuilding image and adding labels: $labels" -echo "FROM $image" | eval docker build $labels -t "$image" - &> /dev/null +Examples: -echo "Rebuilt image '$image' with the following added labels:" -docker image inspect "$image" | jq '.[0].Config.Labels' -echo "Run 'docker push $image' to push it to docker hub" + docker artifact ls infogulch/artifact-test + docker artifact download infogulch/artifact-test /app/othertestfile.txt + docker artifact label infogulch/artifact-test /testfile.txt +" -exit +function main { + case "$1" in + docker-cli-plugin-metadata) + docker_cli_plugin_metadata + ;; + artifact) + case "$2" in + ls) + list "${@:3}" + ;; + label) + label "${@:3}" + ;; + download) + download "${@:3}" + ;; + *) + echo "$__usage" + ;; + esac + ;; + esac +} + +function _search_layer { + local idmap="$1" + local imagetar="$2" + local layertar="$3" + local search="$4" + # look up digest associated with layer path + local digest="$(jq --arg key "$layertar" -r '.[$key]' <<< "$idmap")" + # extract layer from image | list files in layer | add / prefix | search for file | append =$digest to each found file + tar -f "$imagetar" -x "$layertar" -O | tar -t | sed s_^_/_ | grep -wx "$search" | sed 's_.$_\0='"$digest"'_' +} + +function label { + local image="$1" + local searchpath="$2" + local tarfile="$(mktemp).tar" + + # check to see if image exists locally. If not, Docker already prints an error so just exit + if ! docker image inspect "$image" > /dev/null ; then + exit 1 + fi + + # Save image tar to temp file + LOG "Exporting image '$image' to temp file '$tarfile'..." + docker image save "$image" -o "$tarfile" + + # Set up cleanup to not leave image behind + trap "delete_files '$tarfile'; trap - RETURN" RETURN + + # Extract manifest and config file contents + LOG "Collecting metadata from image..." + local manifest="$(tar -xf "$tarfile" -x manifest.json -O | jq)" + local config_file="$(echo "$manifest" | jq -r '.[0].Config')" + local config="$(tar -f "$tarfile" -x "$config_file" -O | jq)" + + # Combine manifest.json and config json to build a map from tar layer directory to layer id sha + local idmap="$(echo "$manifest" "$config" | jq -s '[ [ .[0][0].Layers, .[1].rootfs.diff_ids ] | transpose[] | { (.[0]): .[1] } ] | reduce .[] as $x ({}; . * $x)')" + + # Search each layer for a file matching $searchpath + # TODO also search for related whiteout files that start with `.wh.` + LOG "Searching layers for '$searchpath'..." + local found="$(echo "$manifest" | jq -r '.[0].Layers[]' | xargs -L1 -I {} bash -c '_search_layer "$@"' _ "$idmap" "$tarfile" {} "$searchpath")" + local foundcount="$(echo "$found" | grep -c . || true)" + + # If more than one is found, then bail + if [ $foundcount -gt 1 ]; then + echo "File was changed in multiple layers, aborting. Found files and layer ids:" + echo "$found" + exit 2 + elif [ $foundcount -eq 0 ]; then + echo "No files found matching '$searchpath'" + exit 3 + fi + + local labels="$(echo "$found" | sed 's_^.*$_--label "\0"_' | paste -d' ')" + + # Add a layer to the existing image to add the labels and tag the new image with the same image name + LOG "Rebuilding image and adding labels: $labels" + echo "FROM $image" | eval docker build $labels -t "$image" - &> /dev/null + LOG "All image labels:"$'\n'"$(docker image inspect "$image" | jq '.[0].Config.Labels' | sed 's_^_ _' )" + + # Remind user to push image + echo " [*] Rebuilt image '$image' to add $foundcount labels" + echo " [*] Run 'docker push $image' to push it to your container repository" +} + +function delete_files { + LOG "Cleaning up temp files $@" + rm "$@" +} + +export verbose=1 +function LOG { + [ $verbose ] && >&2 echo -e "$(tput setaf 4) ==> $@$(tput sgr0)" +} + +# Export functions used in subshells +export -f LOG +export -f _search_layer + +main "$@" diff --git a/example/example.sh b/example/example.sh index 5c32356..e31e2ee 100755 --- a/example/example.sh +++ b/example/example.sh @@ -5,11 +5,12 @@ source=$(dirname "${BASH_SOURCE[0]}") pushd "$source" &> /dev/null # Build example image -docker build . -t test-image &> /dev/null +echo "Rebuilding docker image for example" +docker build . -t infogulch/test-image &> /dev/null # Execute docker artifact to add a label to /app/othertestfile.txt set +e -../docker-artifact.sh test-image /app/othertestfile.txt +../docker-artifact.sh artifact label infogulch/test-image /app/othertestfile.txt popd &> /dev/null