commit 3496e778241fc6da6a7afba5cd51aed0e29864e3 Author: garrettmills Date: Tue Jul 30 01:46:59 2024 -0400 Big bang diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..521d78d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/g.bash"] + path = lib/g.bash + url = https://code.garrettmills.dev/garrettmills/g.bash diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/docker-isolate.iml b/.idea/docker-isolate.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/docker-isolate.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..07115cd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..54a237a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a54cb8d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM ubuntu:22.04 + +COPY README.md /README.md + +RUN echo "Fubar!" > /fubar diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfd01d8 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Goal: + +- Specify a base image name +- Specify a final image name +- Identify the last layer of the base image +- Export the intermediate layers that the final image adds to the base image +- Reimport those as a single layer diff --git a/docker-isolate.bash b/docker-isolate.bash new file mode 100755 index 0000000..a27f1f9 --- /dev/null +++ b/docker-isolate.bash @@ -0,0 +1,18 @@ +#!/bin/bash -e + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +. "${SCRIPT_DIR}/lib/g.bash/src/g.bash" + +g::source src/setup +g::source src/isolate + +g::app dlt + +g::app::command isolate "Build a Docker image with only the changed layers between two images" +g::app::command::arg from_image "The base (starting) image, whose layers will be excluded" +g::app::command::arg to_image "The final (ending) image, whose additional layers will be isolated" +g::app::command::flag output= "Path for the output archive" +g::app::command::flag as= "Reimport the isolated filesystem as the specified image name" +g::app::command::flag no-base "When specified with --as, the isolated filesystem will NOT be applied to the base image" + +g::app::invoke "$@" diff --git a/lib/g.bash b/lib/g.bash new file mode 160000 index 0000000..c5141fd --- /dev/null +++ b/lib/g.bash @@ -0,0 +1 @@ +Subproject commit c5141fd19e96066f154937f2d401d76560660d36 diff --git a/lib/jq/README.md b/lib/jq/README.md new file mode 100644 index 0000000..c68d6cb --- /dev/null +++ b/lib/jq/README.md @@ -0,0 +1,5 @@ +https://github.com/jqlang/jq + +`jq`: Command-line JSON processor +Binary version 1.7.1 +Licensed MIT diff --git a/lib/jq/jq b/lib/jq/jq new file mode 100755 index 0000000..37a7a66 Binary files /dev/null and b/lib/jq/jq differ diff --git a/src/dkr_functions.bash b/src/dkr_functions.bash new file mode 100644 index 0000000..e32e952 --- /dev/null +++ b/src/dkr_functions.bash @@ -0,0 +1,26 @@ +#!/bin/bash -e + +function dkr::save_and_extract() { + # image name = $1 + # file name = $2 + g::verbose "Extracting image $1 as $2" + docker save "$1" -o "$2.tar" + mkdir "$2" + cd "$2" + tar xf "../$2.tar" + cd - > /dev/null + rm -f "$2.tar" +} + +function dkr::import_fs_as_layer() { + # tar path = $1 + # base image = $2 + # final image = $3 + docker image import "$1" docker-isolate-flat-temp:latest > /dev/null + echo "FROM docker-isolate-flat-temp:latest as isolated" > Dockerfile + echo "FROM $2 as from_image" >> Dockerfile + echo "COPY --from=isolated / /" >> Dockerfile + docker image build -f Dockerfile -t "$3" . > /dev/null + docker image rm -f docker-isolate-flat-temp:latest > /dev/null + rm -f Dockerfile +} diff --git a/src/isolate.bash b/src/isolate.bash new file mode 100644 index 0000000..6c35318 --- /dev/null +++ b/src/isolate.bash @@ -0,0 +1,82 @@ +#!/bin/bash -e + +g::source dkr_functions + +function app::dlt::isolate() { + local args="$1" + + from_image="$(g::arg "$args" from_image)" + to_image="$(g::arg "$args" to_image)" + g::info "Isolating changes from $from_image to $to_image" + + # Resolve the output path for the tarball since we're going to be changing directories + output_path="$(g::flag "$args" output)" + if [ -z "$output_path" ]; then + output_path="$(pwd)/isolated.tar" + fi + output_path="$(g::path::resolve "$output_path")" + g::verbose "Output path: $output_path" + + g::path::cd "$(g::path::tmpdir)" + g::verbose "Working dir: $(pwd)" + + # Extract both the base and final image in container layer format + dkr::save_and_extract "$to_image" to_image + dkr::save_and_extract "$from_image" from_image + + # Load arrays of BOTH the raw layer tarballs and the SHA sums of the layers in the final image + to_json_path="$(jq -r '.[0].Config' < to_image/manifest.json)" + g::verbose "Found config for to_image: ${to_json_path}" + + readarray -t to_raw_layers <<< "$(jq -r '.[0].Layers[]' < to_image/manifest.json)" + readarray -t to_sha_layers <<< "$(jq -r '.rootfs.diff_ids[]' < "to_image/${to_json_path}")" + + # Load an array of the SHA sums of the layers in the base image + from_json_path="$(jq -r '.[0].Config' < from_image/manifest.json)" + g::verbose "Found config for from_image: ${from_json_path}" + + readarray -t from_sha_layers <<< "$(jq -r '.rootfs.diff_ids[]' < "from_image/${from_json_path}")" + + mkdir isolated + cd isolated + + # Extract the isolated layers + for ((i = 0 ; i < "${#to_raw_layers[@]}" ; i++)); do + raw_layer="${to_raw_layers[$i]}" + sha_layer="${to_sha_layers[$i]}" + + # If the SHA sum of this layer appears in the base image, skip it + if g::arr::includes "$sha_layer" "${from_sha_layers[@]}"; then + g::debug "Skipping layer from base image: $sha_layer" + continue + fi + + # Extract the layer into the isolated directory + g::info "Unpacking layer: $sha_layer -> $raw_layer" + tar xf "../to_image/${raw_layer}" + done + + # Create a tarball w/ the isolated files (special find magic to avoid the leading ./ on paths) + g::info 'Creating archive' + cd .. + find isolated/ \( -type f -o -type d \) -printf "%P\n" | tar cf isolated.tar -C isolated/ -T - + + # Do a bit of cleanup + rm -rf to_image from_image isolated + + # If the user asked for the isolated filesystem to be re-imported as a docker image, do that + as_image="$(g::flag "$args" as)" + if [ -n "$as_image" ]; then + if g::flag::has "$args" no-base; then + g::info "Creating image $as_image without base layers" + docker image import isolated.tar "$as_image" > /dev/null + else + g::info "Creating image $as_image on top of $from_image" + dkr::import_fs_as_layer isolated.tar "$from_image" "$as_image" + fi + fi + + # Create the output archive and exit + g::verbose "Moving archive to final destination" + mv isolated.tar "$output_path" +} diff --git a/src/setup.bash b/src/setup.bash new file mode 100644 index 0000000..3fc614b --- /dev/null +++ b/src/setup.bash @@ -0,0 +1,7 @@ +#!/bin/bash -e + +SRC_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +export PATH="${SRC_DIR}/lib/jq:${PATH}" + +g::log::setLevel info +