diff --git a/bin/strip-docker-image b/bin/strip-docker-image index bf1023c..026d77d 100755 --- a/bin/strip-docker-image +++ b/bin/strip-docker-image @@ -1,154 +1,164 @@ -#!/bin/bash +#!/usr/bin/env bash + # NAME -# strip-docker-image - strips the bare essentials from an image and exports them +# strip-docker-image - strips the bare essentials from an image and exports them # # SYNOPSIS -# strip-docker-image -i image-name -t target-image-name -t [-p package | -f file] [-d Dockerfile] [-x expose-port] [-v] +# strip-docker-image -i image-name -t target-image-name -t [-p package | -f file] [-d Dockerfile] [-x expose-port] [-v] [-k] # # # OPTIONS -# -i image-name to strip -# -t target-image-name the image name of the stripped image -# -p package package to include from image, multiple -p allowed. -# -f file file to include from image, multiple -f allowed. -# -x port to expose. -# -d Dockerfile to incorporate in the stripped image. -# -v verbose +# -i image-name - name of the image to strip +# -t target-image-name - name of the image after stripping +# -p package - package to include from image, multiple parameters allowed +# -f file - file to include from image, multiple parameters allowed +# -x port - port to expose +# -d Dockerfile - Dockerfile to incorporate in the stripped image +# -v - verbose output +# -k - keep stripped container files incl. Dockerfile (for manual editing) # # DESCRIPTION -# creates a new Docker image based on the scratch which contains -# only the the source image of selected packages and files. +# "strip-docker-image" creates a new Docker image based on the scratch which contains +# only the the source image of selected packages and files. # # EXAMPLE -# The following example strips the nginx installation from the default NGiNX docker image, +# The following example strips the nginx installation from the default NGiNX docker image, +# +# strip-docker-image -i nginx -t stripped-nginx \ +# -v \ +# -p nginx \ +# -f /bin/cat \ +# -f /bin/ls \ +# -f /bin/mkdir \ +# -f /bin/ps \ +# -f /bin/sh \ +# -f /etc/group \ +# -f /etc/nsswitch.conf \ +# -f /etc/passwd \ +# -f /var/cache/nginx \ +# -f /var/log/nginx \ +# -f /var/run \ +# -f '/lib/*/libnss*' # -# strip-docker-image -i nginx -t stripped-nginx \ -# -p nginx \ -# -f /etc/passwd \ -# -f /etc/group \ -# -f '/lib/*/libnss*' \ -# -f /bin/ls \ -# -f /bin/cat \ -# -f /bin/sh \ -# -f /bin/mkdir \ -# -f /bin/ps \ -# -f /var/run \ -# -f /var/log/nginx +# CAVEATS +# requires an image that has awk, bash, grep, ldd, readlink, sort & tar installed. # # AUTHOR -# Mark van Holsteijn +# Mark van Holsteijn # # COPYRIGHT # -# Copyright 2015 Xebia Nederland B.V. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2015 Xebia Nederland B.V. # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # +# http://www.apache.org/licenses/LICENSE-2.0 # +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. function usage() { - echo "usage: $(basename $0) -i image-name -t stripped-image-name [-d Dockerfile] [-p package | -f file] [-v]" >&2 - echo " $@" >&2 + echo "usage: ${BASH_SOURCE[0]} -i image-name -t stripped-image-name [-d Dockerfile] [-p package | -f file] [-v] [-k]" >&2 + echo " $@" >&2 } function parse_commandline() { - - while getopts "vi:t:p:f:x:d:" OPT; do - case "$OPT" in - v) - VERBOSE=-v - ;; - p) - PACKAGES="$PACKAGES -p $OPTARG" - ;; - f) - FILES="$FILES -f $OPTARG" - ;; - i) - IMAGE_NAME="$OPTARG" - ;; - t) - TARGET_IMAGE_NAME="$OPTARG" - ;; - x) - EXPOSE_PORTS="$EXPOSE_PORTS $OPTARG" - ;; - d) - DOCKERFILES="$DOCKERFILES \"$OPTARG\"" - ;; - *) - usage - exit 1 - ;; - esac - done - shift $((OPTIND-1)) - - if [ -z "$IMAGE_NAME" ] ; then - usage "image name is missing." - exit 1 - fi - - if [ -z "$TARGET_IMAGE_NAME" ] ; then - usage "target image name -t missing." - exit 1 - fi - - if [ -z "$PACKAGES" -a -z "$FILES" ] ; then - usage "Missing -p or -f options" - exit 1 - fi - export PACKAGES FILES VERBOSE + while getopts "vki:t:p:f:x:d:" OPT; do + case "$OPT" in + v) + VERBOSE=-v + ;; + k) + KEEP=1 + ;; + p) + PACKAGES+=(-p "$OPTARG") + ;; + f) + FILES+=(-f "$OPTARG") + ;; + i) + IMAGE_NAME="$OPTARG" + ;; + t) + TARGET_IMAGE_NAME="$OPTARG" + ;; + x) + EXPOSE_PORTS+=("$OPTARG") + ;; + d) + DOCKERFILES+=("$OPTARG") + ;; + *) + usage + exit 1 + ;; + esac + done + shift $((OPTIND-1)) + + if [ -z "$IMAGE_NAME" ]; then + usage "Image name -i is missing." + exit 1 + fi + + if [ -z "$TARGET_IMAGE_NAME" ]; then + usage "Target image name -t missing." + exit 1 + fi + + if ! (( "${#PACKAGES[@]}" + "${#FILES[@]}" )); then + usage "Missing options -p or -f" + exit 1 + fi } parse_commandline "$@" DIR=strip-docker-image-$$ -mkdir -p $DIR/export -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +mkdir -p "$DIR"/export +SCRIPT_DIR="$PWD" -docker run -v $PWD/$DIR/export:/export \ - -v $SCRIPT_DIR:/mybin \ - --entrypoint="/bin/bash" \ - $IMAGE_NAME \ - /mybin/strip-docker-image-export -d /export $VERBOSE $PACKAGES $FILES +docker run --rm -v "$PWD"/"$DIR"/export:/export \ + -v "$SCRIPT_DIR":/mybin \ + --entrypoint="/bin/bash" \ + "$IMAGE_NAME" \ + /mybin/strip-docker-image-export -d /export \ + "$VERBOSE" "${PACKAGES[@]}" "${FILES[@]}" -cat > $DIR/Dockerfile < "$DIR"/Dockerfile FROM scratch ADD export / -! +EOF -for DOCKERFILE in $DOCKERFILES ; do - sed ':x; /\\$/ { N; s/\\\n//; tx }' "$(eval echo $DOCKERFILE)" \ - | grep -v '^\(FROM\|RUN\|ADD\|COPY\)' \ - >> $DIR/Dockerfile +for DOCKERFILE in "${DOCKERFILES[@]}"; do + sed ':x; /\\$/ { N; s/\\\n//; tx }' "$DOCKERFILE" \ + | grep -v '^\(FROM\|RUN\|ADD\|COPY\)' \ + >> "$DIR"/Dockerfile done -for PORT in $EXPOSE_PORTS ; do - echo EXPOSE $PORT >> $DIR/Dockerfile +for PORT in "${EXPOSE_PORTS[@]}"; do + echo "EXPOSE ${PORT#" "}" >> "$DIR"/Dockerfile done -EXPOSE=$(docker inspect -f '{{range $key, $value := .Config.ExposedPorts}}{{$key}} {{end}}' $IMAGE_NAME | sed -e 's/\/[a-z]* / /g') -ENTRYPOINT=$(docker inspect --format='{{if .Config.Entrypoint}}ENTRYPOINT {{json .Config.Entrypoint}}{{end}}' $IMAGE_NAME) -CMD=$(docker inspect --format='{{if .Config.Cmd}}CMD {{json .Config.Cmd}}{{end}}' $IMAGE_NAME) + EXPOSE=$(docker inspect --format='{{range $key, $value := .Config.ExposedPorts}}{{$key}} {{end}}' "$IMAGE_NAME" | sed -e 's/\/[a-z]* / /g') +ENTRYPOINT=$(docker inspect --format='{{if .Config.Entrypoint}}ENTRYPOINT {{json .Config.Entrypoint}}{{end}}' "$IMAGE_NAME") + CMD=$(docker inspect --format='{{if .Config.Cmd}}CMD {{json .Config.Cmd}}{{end}}' "$IMAGE_NAME") -[ -z "$EXPOSE" ] || echo "EXPOSE $EXPOSE" >> $DIR/Dockerfile -[ -z "$ENTRYPOINT" ] || echo $ENTRYPOINT >> $DIR/Dockerfile -[ -z "$CMD" ] || echo $CMD >> $DIR/Dockerfile +[ -z "$EXPOSE" ] || echo "EXPOSE $EXPOSE" >> "$DIR"/Dockerfile +[ -z "$ENTRYPOINT" ] || echo "$ENTRYPOINT" >> "$DIR"/Dockerfile +[ -z "$CMD" ] || echo "$CMD" >> "$DIR"/Dockerfile -( - cd $DIR - docker build --no-cache -t $TARGET_IMAGE_NAME . -) +pushd "$DIR" &>/dev/null && { + docker build --no-cache -t "$TARGET_IMAGE_NAME" . + popd &>/dev/null +} -rm -rf $DIR +if [ -z "$KEEP" ]; then + rm -rf "$DIR" +fi diff --git a/bin/strip-docker-image-export b/bin/strip-docker-image-export index 9bed7e8..0c88287 100755 --- a/bin/strip-docker-image-export +++ b/bin/strip-docker-image-export @@ -1,178 +1,172 @@ -#!/bin/bash +#!/usr/bin/env bash # NAME -# strip-docker-image-export - exports the bare essentials from a Docker image +# strip-docker-image-export - exports the bare essentials from a Docker image # # SYNOPSIS -# strip-docker-image-export [-d export-dir ] [-p package | -f file] [-v] +# strip-docker-image-export [-d export-dir ] [-p package | -f file] [-v] # # # OPTIONS -# -d export-directory to copy content to, defaults to /export. -# -p package package to include from image, multiple -p allowed. -# -f file file to include from image, multiple -f allowed. -# -v verbose +# -d export-directory - directoy to copy content to, defaults to /export +# -p package - package to include from image, multiple -p allowed +# -f file - file to include from image, multiple -f allowed +# -v - verbose output # # DESCRIPTION -# this script copies all the files from an installed package and copies them -# to an export directory. Additional files can be added. When an executable -# is copied, all dynamic libraries required by the executed are included too. +# This script copies all the files from an installed package and copies them +# to an export directory. Additional files can be added. When an executable +# is copied, all dynamic libraries required by the executed are included too. # # EXAMPLE -# The following example strips the nginx installation from the default NGiNX docker image, -# and allows the files in ./export to be added to a scratch image. +# The following example strips the nginx installation from the default NGiNX docker image, +# and allows the files in ./export to be added to a scratch image. +# +# docker run \ +# -v $PWD/export/:/export \ +# -v $PWD/bin:/mybin nginx \ +# -p nginx \ +# -f /bin/cat \ +# -f /bin/ls \ +# -f /bin/mkdir \ +# -f /bin/ps \ +# -f /bin/sh \ +# -f /etc/group \ +# -f /etc/passwd \ +# -f /var/log/nginx \ +# -f /var/run \ +# -f '/lib/*/libnss*' \ +# -d /export \ +# /mybin/strip-image.sh # -# docker run -v $PWD/export/:/export \ -# -v $PWD/bin:/mybin nginx \ -# /mybin/strip-image.sh \ -# -p nginx \ -# -f /etc/passwd \ -# -f /etc/group \ -# -f '/lib/*/libnss*' \ -# -f /bin/ls \ -# -f /bin/cat \ -# -f /bin/sh \ -# -f /bin/mkdir \ -# -f /bin/ps \ -# -f /var/run \ -# -f /var/log/nginx \ -# -d /export # CAVEATS -# requires an image that has a bash, readlink and ldd installed. +# requires an image that has awk, bash, grep, ldd, readlink, sort & tar installed. # # AUTHOR -# Mark van Holsteijn +# Mark van Holsteijn # # COPYRIGHT # -# Copyright 2015 Xebia Nederland B.V. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at +# Copyright 2015 Xebia Nederland B.V. # -# http://www.apache.org/licenses/LICENSE-2.0 +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. +# http://www.apache.org/licenses/LICENSE-2.0 # +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + export EXPORT_DIR=/export function usage() { - echo "usage: $(basename $0) [-v] [-d export-dir ] [-p package | -f file]" >&2 - echo " $@" >&2 + echo "usage: ${BASH_SOURCE[0]} [-v] [-d export-dir ] [-p package | -f file]" >&2 + echo " $@" >&2 } function parse_commandline() { - - while getopts "vp:f:d:" OPT; do - case "$OPT" in - v) - VERBOSE=v - ;; - p) - PACKAGES="$PACKAGES $OPTARG" - ;; - f) - FILES="$FILES $OPTARG" - ;; - d) - EXPORT_DIR="$OPTARG" - ;; - *) - usage - exit 1 - ;; - esac - done - shift $((OPTIND-1)) - - if [ -z "$PACKAGES" -a -z "$FILES" ] ; then - usage "Missing -p or -f options" - exit 1 - fi - if [ ! -d $EXPORT_DIR ] ; then - usage "$EXPORT_DIR is not a directory." - exit 1 - fi + while getopts "vp:f:d:" OPT; do + case "$OPT" in + v) + VERBOSE=v + ;; + p) + PACKAGES+=("$OPTARG") + ;; + f) + # not quoted -- the script accepts globs as arguments and expands them here + FILES+=($OPTARG) + ;; + d) + EXPORT_DIR="$OPTARG" + ;; + *) + usage + exit 1 + ;; + esac + done + shift $((OPTIND-1)) + + if ! (( "${#PACKAGES[@]}" + "${#FILES[@]}" )); then + usage "Missing options -p or -f" + exit 1 + fi + if [ ! -d "$EXPORT_DIR" ]; then + usage "$EXPORT_DIR is not a directory." + exit 1 + fi } function print_file() { - if [ "$1" = "/usr/bin/ldd" ]; then - exit - fi - - if [ -e "$1" ] ; then - echo "$1" - else - test -n "$VERBOSE" && echo "INFO: ignoring not existent file '$1'" >&2 - fi - - if [ -s "$1" ] ; then - TARGET=$(readlink "$1") - if [ -n "$TARGET" ] ; then - if expr "$TARGET" : '^/' >/dev/null 2>&1 ; then - list_dependencies "$TARGET" - else - list_dependencies $(dirname "$1")/"$TARGET" - fi - fi - fi + if [ "$1" = "/usr/bin/ldd" ]; then + exit + fi + + if [ -e "$1" ]; then + printf '%s\0' "$1" + else + test -n "$VERBOSE" && echo "INFO: ignoring not existent file '$1'" >&2 + fi + + if [ -s "$1" ]; then + TARGET="$(readlink "$1")" + if [ -n "$TARGET" ]; then + if expr "$TARGET" : '^/' >/dev/null 2>&1; then + list_dependencies "$TARGET" + else + list_dependencies "$(dirname "$1")"/"$TARGET" + fi + fi + fi } function list_dependencies() { - for FILE in $@ ; do - if [ -e "$FILE" ] ; then - print_file "$FILE" - if /usr/bin/ldd "$FILE" >/dev/null 2>&1 ; then - /usr/bin/ldd "$FILE" | \ - awk '/statically/{next;} /=>/ { print $3; next; } { print $1 }' | \ - while read LINE ; do - test -n "$VERBOSE" && echo "INFO: including $LINE" >&2 - print_file "$LINE" - done - fi - else - test -n "$VERBOSE" && echo "INFO: ignoring not existent file $FILE" >&2 - fi - done + for FILE in "$@"; do + if [ -e "$FILE" ]; then + print_file "$FILE" + if /usr/bin/ldd "$FILE" >/dev/null 2>&1 ; then + /usr/bin/ldd "$FILE" | \ + awk '/statically/{next;} /=>/ { print $3; next; } { print $1 }' | \ + while read -r LINE; do + test -n "$VERBOSE" && echo "INFO: including $LINE" >&2 + print_file "$LINE" + done + fi + else + test -n "$VERBOSE" && echo "INFO: ignoring not existent file $FILE" >&2 + fi + done } function list_packages() { - if test -e /usr/bin/dpkg; then - DEPS=$(/usr/bin/dpkg -L $1) - elif test -e /usr/bin/rpm; then - DEPS=$(/usr/bin/rpm -ql $1) - elif test -e /sbin/apk; then - DEPS=$(/sbin/apk info -L $1 | grep -Ev '^$|contains:' | sed 's/^/\//g') - else - echo 'WARN: Unknown OS, aborted list_packages()' - exit - fi - while read FILE ; do - if [ ! -d "$FILE" ] ; then - list_dependencies "$FILE" - fi + if test -e /usr/bin/dpkg; then + DEPS=$(/usr/bin/dpkg -L "$@") + elif test -e /usr/bin/rpm; then + DEPS=$(/usr/bin/rpm -ql "$@") + elif test -e /sbin/apk; then + DEPS=$(/sbin/apk info -L "$@" | grep -Ev '^$|contains:' | sed 's/^/\//g') + else + echo 'WARN: Unknown OS, aborted list_packages()' + exit + fi + while read -r FILE; do + if [ ! -d "$FILE" ]; then + list_dependencies "$FILE" + fi done <<< "$DEPS" } function list_all_packages() { - for i in "$@" ; do - list_packages "$i" - done + for i in "$@"; do + list_packages "$i" + done } parse_commandline "$@" - - -tar czf - $( - - ( - list_all_packages $PACKAGES - list_dependencies $FILES - ) | sort -u - -) | ( cd $EXPORT_DIR ; tar -xzh${VERBOSE}f - ) +{ list_all_packages "${PACKAGES[@]}"; list_dependencies "${FILES[@]}"; } | sort --zero-terminated --unique > files; +tar --null -cf - --files-from=files | tar -C "$EXPORT_DIR" -xh"${VERBOSE}"f -