├── .copr └── Makefile ├── .gitignore ├── CODE-OF-CONDUCT.md ├── Dockerfile ├── SECURITY.md ├── contrib └── cirrus │ ├── build_and_test.sh │ ├── setup.sh │ ├── timestamp.awk │ ├── ooe.sh │ ├── lib.sh.t │ └── lib.sh ├── test ├── helpers.bash ├── 00-simple.bats ├── 20-unpack.bats ├── 14-extra_src_dirs.bats ├── 12-from_rpms_push.bats └── 10-from_rpms.bats ├── developing.md ├── BuildSourceImage.spec.in ├── .github └── renovate.json5 ├── README.md ├── Makefile ├── .cirrus.yml ├── layout.md ├── LICENSE └── BuildSourceImage.sh /.copr/Makefile: -------------------------------------------------------------------------------- 1 | ../Makefile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .build-container 3 | .testprep 4 | .validate 5 | *.rpm 6 | x86_64/ 7 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## The BuildSourceImage Project Community Code of Conduct 2 | 3 | The BuildSourceImage project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md). 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/skopeo/stable 2 | 3 | RUN dnf install -y jq findutils file wget 'dnf-command(download)' 4 | 5 | COPY ./BuildSourceImage.sh /usr/local/bin/BuildSourceImage.sh 6 | 7 | ENV BASE_DIR=/tmp 8 | 9 | ENTRYPOINT ["/usr/local/bin/BuildSourceImage.sh"] 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security and Disclosure Information Policy for the BuildSourceImage Project 2 | 3 | The BuildSourceImage Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects. 4 | -------------------------------------------------------------------------------- /contrib/cirrus/build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | source $(dirname $0)/lib.sh 6 | 7 | cd $CIRRUS_WORKING_DIR 8 | 9 | showrun echo "Validating..." 10 | showrun make validate 11 | 12 | showrun echo "Building container image..." 13 | showrun make validate 14 | 15 | showrun echo "Running integration tests..." 16 | showrun make test-integration 17 | -------------------------------------------------------------------------------- /test/helpers.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export CTR_IMAGE="${CTR_IMAGE:-localhost/containers/buildsourceimage}" 4 | export CTR_ENGINE="${CTR_ENGINE:-podman}" 5 | 6 | function run_ctr() { 7 | run $CTR_ENGINE run --security-opt label=disable --rm "$@" 8 | # Debugging bats tests can be challenging without seeing std{err,out} 9 | # of the executed processes. The echo below will only be printed when 10 | # the command fails and ultimately eases debugging. 11 | echo "${lines}" 12 | } 13 | -------------------------------------------------------------------------------- /contrib/cirrus/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | FEDORA_PACKAGES=" 6 | bats 7 | podman 8 | ShellCheck 9 | skopeo 10 | wget 11 | " 12 | 13 | source $(dirname $0)/lib.sh 14 | 15 | show_env_vars 16 | 17 | install_ooe 18 | 19 | # When the fedora repos go down, it tends to last quite a while :( 20 | timeout_attempt_delay_command 120s 3 120s dnf install -y \ 21 | '@C Development Tools and Libraries' '@Development Tools' \ 22 | $FEDORA_PACKAGES 23 | -------------------------------------------------------------------------------- /contrib/cirrus/timestamp.awk: -------------------------------------------------------------------------------- 1 | 2 | 3 | # This script is intended to be piped into by automation, in order to 4 | # mark output lines with timing information. For example: 5 | # /path/to/command |& awk --file timestamp.awk 6 | 7 | BEGIN { 8 | STARTTIME=systime() 9 | printf "[%s] START", strftime("%T") 10 | printf " - All [+xxxx] lines that follow are relative to right now.\n" 11 | } 12 | 13 | { 14 | printf "[%+05ds] %s\n", systime()-STARTTIME, $0 15 | } 16 | 17 | END { 18 | printf "[%s] END", strftime("%T") 19 | printf " - [%+05ds] total duration since START\n", systime()-STARTTIME 20 | } 21 | -------------------------------------------------------------------------------- /developing.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | ## Requirements 4 | 5 | * `make` 6 | * `shellcheck` (package `ShellCheck` on fedora) 7 | * `bats` 8 | * `wget` 9 | * `podman` (or `docker`) 10 | * `jq` 11 | 12 | ## Lint 13 | 14 | [ShellCheck](https://www.shellcheck.net/) is used to ensure the shell script is nice and tidy. 15 | 16 | ```bash 17 | make validate 18 | ``` 19 | 20 | ## Tests 21 | 22 | Testing is done with [`bats`](https://github.com/bats-core/bats-core). 23 | 24 | While it's possible to kick the tests by calling `bats ./test/`, many of the tests are written to use the script as built into a container image. 25 | If you are making local changes and have not rebuilt the container, then they will be missed. 26 | 27 | Best to kick off the build like: 28 | ```bash 29 | make test-integration 30 | ``` 31 | This will rebuild the container if needed before running the tests. 32 | 33 | ## -------------------------------------------------------------------------------- /test/00-simple.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -t 2 | 3 | load helpers 4 | 5 | @test "Help" { 6 | run_ctr $CTR_IMAGE -h 7 | [ "$status" -eq 0 ] 8 | [[ ${lines[0]} =~ "BuildSourceImage.sh version " ]] 9 | [[ ${lines[1]} =~ "Usage: BuildSourceImage.sh " ]] 10 | } 11 | 12 | @test "Version" { 13 | run_ctr $CTR_IMAGE -v 14 | [ "$status" -eq 0 ] 15 | [[ ${lines[0]} =~ "BuildSourceImage.sh version " ]] 16 | } 17 | 18 | @test "List Drivers" { 19 | run_ctr $CTR_IMAGE -l 20 | [ "$status" -eq 0 ] 21 | [[ ${lines[0]} =~ "sourcedriver_context_dir" ]] 22 | [[ ${lines[1]} =~ "sourcedriver_extra_src_dir" ]] 23 | [[ ${lines[2]} =~ "sourcedriver_rpm_dir" ]] 24 | [[ ${lines[3]} =~ "sourcedriver_rpm_fetch" ]] 25 | } 26 | 27 | @test "No input" { 28 | run_ctr $CTR_IMAGE 29 | [ "$status" -eq 1 ] 30 | [[ ${lines[0]} =~ "[SrcImg][ERROR] provide an input (example: BuildSourceImage.sh -e ./my-sources/ )" ]] 31 | } 32 | -------------------------------------------------------------------------------- /test/20-unpack.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -t 2 | 3 | load helpers 4 | 5 | @test "unpack - no args" { 6 | run_ctr $CTR_IMAGE unpack 7 | [ "$status" -eq 1 ] 8 | [[ ${lines[0]} =~ "[SrcImg][ERROR] [unpack_img] blank arguments provided" ]] 9 | } 10 | 11 | @test "unpack - Help" { 12 | run_ctr $CTR_IMAGE unpack -h 13 | [ "$status" -eq 1 ] 14 | [[ ${lines[0]} =~ "BuildSourceImage.sh unpack " ]] 15 | } 16 | 17 | @test "unpack - from a SRPM build" { 18 | local d 19 | local r 20 | 21 | d=$(mktemp -d) 22 | echo "temporary directories: output - ${d}" 23 | run_ctr -v $(pwd)/.testprep/srpms/:/src:ro --mount type=bind,source=${d},destination=/output $CTR_IMAGE -s /src -o /output 24 | [ "$status" -eq 0 ] 25 | [ -f "${d}/index.json" ] 26 | 27 | r=$(mktemp -d) 28 | echo "temporary directories: unpacked - ${r}" 29 | run_ctr --mount type=bind,source=${d},destination=/output -v ${r}:/unpacked/ $CTR_IMAGE unpack /output/ /unpacked/ 30 | [ "$(find ${r} -type f | wc -l)" -eq 3 ] # regular files 31 | [ "$(find ${r} -type l | wc -l)" -eq 3 ] # and symlinks 32 | } 33 | -------------------------------------------------------------------------------- /contrib/cirrus/ooe.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script executes a command while logging all output to a temporary 4 | # file. If the command exits non-zero, then all output is sent to the console, 5 | # before returning the exit code. If the script itself fails, the exit code 121 6 | # is returned. 7 | 8 | set -eo pipefail 9 | 10 | SCRIPT_BASEDIR="$(basename $0)" 11 | 12 | badusage() { 13 | echo "Incorrect usage: $SCRIPT_BASEDIR) [options]" > /dev/stderr 14 | echo "ERROR: $1" 15 | exit 121 16 | } 17 | 18 | COMMAND="$@" 19 | [[ -n "$COMMAND" ]] || badusage "No command specified" 20 | 21 | OUTPUT_TMPFILE="$(mktemp -p '' ${SCRIPT_BASEDIR}_output_XXXX)" 22 | output_on_error() { 23 | RET=$? 24 | set +e 25 | if [[ "$RET" -ne "0" ]] 26 | then 27 | echo "---------------------------" 28 | cat "$OUTPUT_TMPFILE" 29 | echo "[$(date --iso-8601=second)] $COMMAND" 30 | fi 31 | rm -f "$OUTPUT_TMPFILE" 32 | } 33 | trap "output_on_error" EXIT 34 | 35 | "$@" 2>&1 | while IFS='' read LINE # Preserve leading/trailing whitespace 36 | do 37 | # Every stdout and (copied) stderr line 38 | echo "[$(date --iso-8601=second)] $LINE" 39 | done >> "$OUTPUT_TMPFILE" 40 | -------------------------------------------------------------------------------- /test/14-extra_src_dirs.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -t 2 | 3 | load helpers 4 | 5 | @test "build with multiple extra source directories" { 6 | local d 7 | d=$(mktemp -d) 8 | echo "temporary directory: ${d}" 9 | 10 | extra_dir1=$(mktemp -d) 11 | echo 123 > $extra_dir1/123.txt 12 | extra_dir2=$(mktemp -d) 13 | echo 456 > $extra_dir1/456.txt 14 | 15 | run_ctr -v $(pwd)/.testprep/srpms/:/src:ro --mount type=bind,source=${d},destination=/output $CTR_IMAGE -e $extra_dir1 -e $extra_dir2 -o /output 16 | [ "$status" -eq 0 ] 17 | echo ${lines} 18 | [[ ${lines[0]} =~ "[SrcImg][INFO] calling source collection drivers" ]] 19 | [[ ${lines[3]} =~ "[SrcImg][INFO] adding extra source directory $extra_dir1" ]] 20 | # get the number of the last line 21 | n=$(expr ${#lines[@]} - 1) 22 | [[ ${lines[${n}]} =~ "[SrcImg][INFO] copied to oci:/output:latest-source" ]] 23 | 24 | echo "${d}" 25 | [ -f "${d}/index.json" ] 26 | [ -f "${d}/oci-layout" ] 27 | [ "$(du -b ${d}/index.json | awk '{ print $1 }')" -gt 0 ] 28 | [ "$(du -b ${d}/oci-layout | awk '{ print $1 }')" -gt 0 ] 29 | 30 | # let's press that the files are predictable 31 | [ "$(find ${d} -type f | wc -l)" -eq 6 ] 32 | [ -f "${d}/blobs/sha256/124edef61b84f2d3562d33780906711943c04b882468840f80bb0c7b11046a1a" ] 33 | [ -f "${d}/blobs/sha256/61a2c454ebd7357237f27ebe452c6eae5b7d1525405661444d93aadbe0771e95" ] 34 | } 35 | -------------------------------------------------------------------------------- /test/12-from_rpms_push.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -t 2 | 3 | load helpers 4 | 5 | # To test skopeo-copy in the provided container image, we are running a local 6 | # registry that we're starting and shutting down on-demand. 7 | 8 | setup() { 9 | run $CTR_ENGINE run -d --name bsi-test-registry --net=host -p 5000:5000 registry:2 10 | } 11 | 12 | teardown() { 13 | run $CTR_ENGINE rm -f bsi-test-registry 14 | } 15 | 16 | @test "build from RPMS and push to local registry" { 17 | skip "deprecating push/pull. Use 'skopeo' instead" 18 | 19 | local d 20 | d=$(mktemp -d) 21 | echo "temporary directory: ${d}" 22 | 23 | run_ctr -v $(pwd)/.testprep/srpms/:/src:ro --net=host --mount type=bind,source=${d},destination=/output $CTR_IMAGE -s /src -o /output -p docker://localhost:5000/output:latest-source 24 | [ "$status" -eq 0 ] 25 | 26 | echo "${d}" 27 | [ -f "${d}/index.json" ] 28 | [ -f "${d}/oci-layout" ] 29 | [ "$(du -b ${d}/index.json | awk '{ print $1 }')" -gt 0 ] 30 | [ "$(du -b ${d}/oci-layout | awk '{ print $1 }')" -gt 0 ] 31 | 32 | # let's press that the files are predictable 33 | [ "$(find ${d} -type f | wc -l)" -eq 7 ] 34 | [ -f "${d}/blobs/sha256/5266b3106c38b4535e314ff52faa3bcf1e9c8256738469381e147c81d700201a" ] 35 | [ -f "${d}/blobs/sha256/56fb92b015150dd20c581f3a15035a67bc017f41d3115e9ce526a760e27acdfb" ] 36 | [ -f "${d}/blobs/sha256/6fd6b2113b8afdd00c25585f75330039981ad3d59a63c5f7d45707f1bdc7bafe" ] 37 | 38 | # now let's pull the image with skopeo 39 | mkdir ${d}/pull 40 | run skopeo copy --src-tls-verify=false docker://localhost:5000/output:latest-source dir:${d}/pull 41 | } 42 | -------------------------------------------------------------------------------- /test/10-from_rpms.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats -t 2 | 3 | load helpers 4 | 5 | @test "build from RPMS" { 6 | local d 7 | d=$(mktemp -d) 8 | echo "temporary directory: ${d}" 9 | 10 | run_ctr -v $(pwd)/.testprep/srpms/:/src:ro --mount type=bind,source=${d},destination=/output $CTR_IMAGE -s /src -o /output 11 | [ "$status" -eq 0 ] 12 | echo ${lines} 13 | [[ ${lines[0]} =~ "[SrcImg][INFO] calling source collection drivers" ]] 14 | # get the number of the last line 15 | n=$(expr ${#lines[@]} - 1) 16 | [[ ${lines[${n}]} =~ "[SrcImg][INFO] copied to oci:/output:latest-source" ]] 17 | 18 | echo "${d}" 19 | [ -f "${d}/index.json" ] 20 | [ -f "${d}/oci-layout" ] 21 | [ "$(du -b ${d}/index.json | awk '{ print $1 }')" -gt 0 ] 22 | [ "$(du -b ${d}/oci-layout | awk '{ print $1 }')" -gt 0 ] 23 | 24 | # let's press that the files are predictable 25 | [ "$(find ${d} -type f | wc -l)" -eq 7 ] 26 | [ -f "${d}/blobs/sha256/505859f8f59319728e8551c89599b213a76c33181eb853abad1d23bd18a43330" ] 27 | [ -f "${d}/blobs/sha256/af9ba810f4cbe017de443c5fe38f1fd64d65b0a74d5c98d9282645284d25a271" ] 28 | [ -f "${d}/blobs/sha256/dd000c5d3a7cdef9d19f74986875ddc7de37c7376fd3c7aba57139e946e022ff" ] 29 | } 30 | 31 | @test "build from RPMS and push" { 32 | skip "deprecating push/pull. Use 'skopeo' instead." 33 | local d 34 | d=$(mktemp -d) 35 | echo "temporary directory: ${d}" 36 | 37 | run_ctr -v $(pwd)/.testprep/srpms/:/src:ro --mount type=bind,source=${d},destination=/output $CTR_IMAGE -s /src -p oci:/output/pushed-image:latest-source 38 | [ "$status" -eq 0 ] 39 | 40 | run ls ${d}/pushed-image 41 | [ "$status" -eq 0 ] 42 | } 43 | -------------------------------------------------------------------------------- /BuildSourceImage.spec.in: -------------------------------------------------------------------------------- 1 | %global git_commit GITCOMMIT 2 | %global git_shortcommit %(c=%{git_commit}; echo ${c:0:7}) 3 | 4 | Name: BuildSourceImage 5 | Version: MYVERSION.TIMESTAMP.git%{git_shortcommit} 6 | Release: 1%{?dist} 7 | Summary: Container Source Image tool 8 | 9 | Group: containers 10 | License: GPLv2 11 | URL: https://github.com/containers/BuildSourceImage 12 | Source0: BuildSourceImage.sh 13 | Source1: LICENSE 14 | Source2: README.md 15 | Source3: layout.md 16 | 17 | #BuildRequires: 18 | Requires: jq 19 | Requires: skopeo 20 | Requires: findutils 21 | Requires: file 22 | %if 0%{?rhel} > 6 23 | Requires: yum-utils 24 | %else 25 | Requires: dnf-command(download) 26 | %endif 27 | 28 | %description 29 | %{summary}. 30 | 31 | %prep 32 | 33 | 34 | %build 35 | 36 | 37 | %install 38 | %{__mkdir_p} %{buildroot}/%{_bindir} 39 | %{__mkdir_p} %{buildroot}/%{_defaultlicensedir}/%{name} 40 | %{__mkdir_p} %{buildroot}/%{_defaultdocdir}/%{name} 41 | %{__install} -T -m 0755 ${RPM_SOURCE_DIR}/BuildSourceImage.sh %{buildroot}/%{_bindir}/BuildSourceImage 42 | %{__install} -T -m 0644 ${RPM_SOURCE_DIR}/LICENSE %{buildroot}/%{_defaultlicensedir}/%{name}/LICENSE 43 | %{__install} -T -m 0644 ${RPM_SOURCE_DIR}/README.md %{buildroot}/%{_defaultdocdir}/%{name}/README.md 44 | %{__install} -T -m 0644 ${RPM_SOURCE_DIR}/layout.md %{buildroot}/%{_defaultdocdir}/%{name}/layout.md 45 | 46 | 47 | %files 48 | %doc %{_defaultlicensedir}/%{name}/LICENSE 49 | %doc %{_defaultdocdir}/%{name}/README.md 50 | %doc %{_defaultdocdir}/%{name}/layout.md 51 | %{_bindir}/BuildSourceImage 52 | 53 | 54 | 55 | %changelog 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | /* 2 | Renovate is a service similar to GitHub Dependabot, but with 3 | (fantastically) more configuration options. So many options 4 | in fact, if you're new I recommend glossing over this cheat-sheet 5 | prior to the official documentation: 6 | 7 | https://www.augmentedmind.de/2021/07/25/renovate-bot-cheat-sheet 8 | 9 | Configuration Update/Change Procedure: 10 | 1. Make changes 11 | 2. Manually validate changes (from repo-root): 12 | 13 | podman run -it \ 14 | -v ./.github/renovate.json5:/usr/src/app/renovate.json5:z \ 15 | docker.io/renovate/renovate:latest \ 16 | renovate-config-validator 17 | 3. Commit. 18 | 19 | Configuration Reference: 20 | https://docs.renovatebot.com/configuration-options/ 21 | 22 | Monitoring Dashboard: 23 | https://app.renovatebot.com/dashboard#github/containers 24 | 25 | Note: The Renovate bot will create/manage it's business on 26 | branches named 'renovate/*'. Otherwise, and by 27 | default, the only the copy of this file that matters 28 | is the one on the `main` branch. No other branches 29 | will be monitored or touched in any way. 30 | */ 31 | 32 | { 33 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 34 | 35 | /************************************************* 36 | ****** Global/general configuration options ***** 37 | *************************************************/ 38 | 39 | // Re-use predefined sets of configuration options to DRY 40 | "extends": [ 41 | // https://github.com/containers/automation/blob/main/renovate/defaults.json5 42 | "github>containers/automation//renovate/defaults.json5" 43 | ], 44 | 45 | // Permit automatic rebasing when base-branch changes by more than 46 | // one commit. 47 | "rebaseWhen": "behind-base-branch", 48 | 49 | /************************************************* 50 | *** Repository-specific configuration options *** 51 | *************************************************/ 52 | 53 | "assignees": ["vrothberg", "rhatdan"], 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.cirrus-ci.com/github/containers/BuildSourceImage.svg)](https://cirrus-ci.com/github/containers/BuildSourceImage/master) 2 | [![Container Image Repository on Quay](https://quay.io/repository/ctrs/bsi/status "Container Image Repository on Quay")](https://quay.io/repository/ctrs/bsi) 3 | 4 | # BuildSourceImage 5 | 6 | Tool to build a source image. 7 | The goal is to make retrieving the source code used to make a container image 8 | easier for users to obtain, using the standard OCI protocols and image formats. 9 | 10 | ## Usage 11 | 12 | ```bash 13 | $> ./BuildSourceImage.sh -h 14 | BuildSourceImage.sh version 0.1 15 | Usage: BuildSourceImage.sh [-D] [-b ] [-c ] [-e ] [-r ] [-o ] [-p ] [-l] [-d ] 16 | 17 | -b base path for source image builds 18 | -c build context for the container image. Can be provided via CONTEXT_DIR env variable 19 | -e extra src for the container image. Can be provided via EXTRA_SRC_DIR env variable 20 | -s directory of SRPMS to add. Can be provided via SRPM_DIR env variable 21 | -o output the OCI image to path. Can be provided via OUTPUT_DIR env variable 22 | -d enumerate specific source drivers to run 23 | -l list the source drivers available 24 | -p push source image to specified reference after build 25 | -D debuging output. Can be set via DEBUG env variable 26 | -h this usage information 27 | -v version 28 | 29 | ``` 30 | 31 | Nicely usable inside a container: 32 | 33 | ```bash 34 | $> mkdir ./output/ 35 | $> podman run -it -v $(pwd)/output/:/output/ -v $(pwd)/SRCRPMS/:/data/ -u $(id -u) quay.io/ctrs/bsi -s /data/ -o /output/ 36 | ``` 37 | 38 | ## Examples 39 | 40 | * Building from a fetched reference [![asciicast](https://asciinema.org/a/266340.svg)](https://asciinema.org/a/266340) 41 | * Building from a directory of src.rpms: [![asciicast](https://asciinema.org/a/266341.svg)](https://asciinema.org/a/266341) 42 | * Building from a directory of src.rpms and pushing it to a simple registry: [![asciicast](https://asciinema.org/a/266343.svg)](https://asciinema.org/a/266343) 43 | 44 | ## Use Cases 45 | 46 | * Build a source image from an existing container image by introspection 47 | * Build a source code image from a collection of known `.src.rpm`'s 48 | * Include additional build context into the source image 49 | * Include extra sources use 50 | -------------------------------------------------------------------------------- /contrib/cirrus/lib.sh.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Unit tests for some functions in lib.sh 4 | # 5 | source $(dirname $0)/lib.sh 6 | 7 | # Iterator and return code; updated in test functions 8 | testnum=0 9 | rc=0 10 | 11 | function check_result { 12 | testnum=$(expr $testnum + 1) 13 | MSG=$(echo "$1" | tr -d '*>\012'|sed -e 's/^ \+//') 14 | if [ "$MSG" = "$2" ]; then 15 | echo "ok $testnum $3 = $MSG" 16 | else 17 | echo "not ok $testnum $3" 18 | echo "# expected: $2" 19 | echo "# actual: $MSG" 20 | rc=1 21 | fi 22 | } 23 | 24 | ############################################################################### 25 | # tests for die() 26 | 27 | function test_die() { 28 | local input_status=$1 29 | local input_msg=$2 30 | local expected_status=$3 31 | local expected_msg=$4 32 | 33 | local msg 34 | msg=$(die $input_status "$input_msg") 35 | local status=$? 36 | 37 | check_result "$msg" "$expected_msg" "die $input_status $input_msg" 38 | } 39 | 40 | test_die 1 "a message" 1 "a message" 41 | test_die 2 "" 2 "FATAL ERROR (but no message given!) in test_die()" 42 | test_die '' '' 1 "FATAL ERROR (but no message given!) in test_die()" 43 | 44 | ############################################################################### 45 | # tests for req_env_var() 46 | 47 | function test_rev() { 48 | local input_args=$1 49 | local expected_status=$2 50 | local expected_msg=$3 51 | 52 | # bash gotcha: doing 'local msg=...' on one line loses exit status 53 | local msg 54 | msg=$(req_env_var $input_args) 55 | local status=$? 56 | 57 | check_result "$msg" "$expected_msg" "req_env_var $input_args" 58 | check_result "$status" "$expected_status" "req_env_var $input_args (rc)" 59 | } 60 | 61 | # error if called with no args 62 | test_rev '' 1 'FATAL: req_env_var: invoked without arguments' 63 | 64 | # error if desired envariable is unset 65 | unset FOO BAR 66 | test_rev FOO 9 'FATAL: test_rev() requires $FOO to be non-empty' 67 | test_rev BAR 9 'FATAL: test_rev() requires $BAR to be non-empty' 68 | # OK if desired envariable was unset 69 | FOO=1 70 | test_rev FOO 0 '' 71 | 72 | # OK if multiple vars are non-empty 73 | FOO="stuff" 74 | BAR="things" 75 | ENV_VARS="FOO BAR" 76 | test_rev "$ENV_VARS" 0 '' 77 | unset BAR 78 | 79 | # ...but error if any single desired one is unset 80 | test_rev "FOO BAR" 9 'FATAL: test_rev() requires $BAR to be non-empty' 81 | 82 | # ...and OK if all args are set 83 | BAR=1 84 | test_rev "FOO BAR" 0 '' 85 | 86 | ############################################################################### 87 | 88 | exit $rc 89 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | pkgname := BuildSourceImage 2 | CTR_IMAGE := localhost/containers/buildsourceimage 3 | CTR_ENGINE ?= podman 4 | BATS_OPTS ?= 5 | cleanfiles = 6 | # these are packages whose src.rpms are very small 7 | srpm_urls = \ 8 | https://archive.kernel.org/centos-vault/7.0.1406/os/Source/SPackages/basesystem-10.0-7.el7.centos.src.rpm \ 9 | https://archive.kernel.org/centos-vault/7.0.1406/os/Source/SPackages/rootfiles-8.1-11.el7.src.rpm \ 10 | https://archive.kernel.org/centos-vault/7.0.1406/os/Source/SPackages/centos-bookmarks-7-1.el7.src.rpm 11 | srpms = $(addprefix ./.testprep/srpms/,$(notdir $(rpms))) 12 | 13 | spec := $(pkgname).spec 14 | cwd := $(shell dirname $(shell realpath $(spec))) 15 | NAME = $(pkgname) 16 | ifeq (,$(shell command -v git)) 17 | gitcommit := HEAD 18 | gitdate := $(shell date +%s) 19 | else 20 | gitcommit := $(shell git rev-parse --verify HEAD) 21 | gitdate := $(shell git log -1 --pretty=format:%ct $(gitcommit)) 22 | endif 23 | VERSION := 0.2.0 24 | RELEASE = $(shell rpmspec -q --qf "%{release}" $(spec) 2>/dev/null) 25 | ARCH = $(shell rpmspec -q --qf "%{arch}" $(spec) 2>/dev/null) 26 | NVR = $(NAME)-$(VERSION)-$(RELEASE) 27 | outdir ?= $(cwd) 28 | 29 | SHELL_SRC := ./BuildSourceImage.sh 30 | DIST_FILES := \ 31 | $(SHELL_SRC) \ 32 | LICENSE \ 33 | layout.md \ 34 | README.md 35 | 36 | export CTR_IMAGE 37 | export CTR_ENGINE 38 | 39 | all: validate 40 | 41 | validate: .validate 42 | 43 | cleanfiles += .validate 44 | .validate: $(SHELL_SRC) 45 | shellcheck $(SHELL_SRC) && touch $@ 46 | 47 | build-container: .build-container 48 | 49 | cleanfiles += .build-container 50 | .build-container: .validate Dockerfile $(SHELL_SRC) 51 | @echo 52 | @echo "==> Building BuildSourceImage Container" 53 | $(CTR_ENGINE) build --quiet --file Dockerfile --tag $(CTR_IMAGE) . && touch $@ 54 | 55 | cleanfiles += .testprep $(srpms) 56 | .testprep: 57 | @echo "==> Fetching SRPMs for testing against" 58 | mkdir -p $@/{srpms,tmp} 59 | wget -P $@/srpms/ $(srpm_urls) 60 | 61 | .PHONY: test-integration 62 | test-integration: .build-container .testprep 63 | @echo 64 | @echo "==> Running integration tests" 65 | TMPDIR=$(realpath .testprep/tmp) bats $(BATS_OPTS) test/ 66 | 67 | .PHONY: srpm 68 | srpm: $(NVR).src.rpm 69 | @echo $^ 70 | 71 | .PHONY: $(spec) 72 | cleanfiles += $(spec) 73 | $(spec): $(spec).in 74 | sed \ 75 | 's/MYVERSION/$(VERSION)/g; s/GITCOMMIT/$(gitcommit)/g; s/TIMESTAMP/$(gitdate)/g' \ 76 | $(spec).in > $(spec) 77 | 78 | cleanfiles += $(NVR).src.rpm 79 | $(NVR).src.rpm: $(spec) $(DIST_FILES) 80 | rpmbuild \ 81 | --define '_sourcedir $(cwd)' \ 82 | --define '_specdir $(cwd)' \ 83 | --define '_builddir $(cwd)' \ 84 | --define '_srcrpmdir $(outdir)' \ 85 | --define '_rpmdir $(outdir)' \ 86 | --nodeps \ 87 | -bs $(spec) 88 | 89 | .PHONY: rpm 90 | rpm: $(ARCH)/$(NVR).$(ARCH).rpm 91 | @echo $^ 92 | 93 | cleanfiles += $(ARCH)/$(NVR).$(ARCH).rpm 94 | $(ARCH)/$(NVR).$(ARCH).rpm: $(spec) $(DIST_FILES) 95 | rpmbuild \ 96 | --define '_sourcedir $(cwd)' \ 97 | --define '_specdir $(cwd)' \ 98 | --define '_builddir $(cwd)' \ 99 | --define '_srcrpmdir $(outdir)' \ 100 | --define '_rpmdir $(outdir)' \ 101 | -bb ./$(spec) 102 | 103 | clean: 104 | if [ -n "$(cleanfiles)" ] ; then rm -rf $(cleanfiles) ; fi 105 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Main collection of env. vars to set for all tasks and scripts. 4 | env: 5 | CIRRUS_WORKING_DIR: "/tmp/github.com/containers/BuildSourceImage" 6 | SCRIPT_BASE: "./contrib/cirrus" 7 | CIRRUS_SHELL: "/bin/bash" 8 | IMAGE_PROJECT: "libpod-218412" 9 | HOME: "/root" # not set by default 10 | 11 | #### Cache-image names to test with 12 | #### 13 | # GCE project where images live 14 | IMAGE_PROJECT: "libpod-218412" 15 | IMAGE_SUFFIX: "c20240513t140131z-f40f39d13" 16 | FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" 17 | 18 | #### 19 | #### Command variables to help avoid duplication 20 | #### 21 | # Command to prefix every output line with a timestamp 22 | # (can't do inline awk script, Cirrus-CI or YAML mangles quoting) 23 | _TIMESTAMP: 'awk -f ${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/timestamp.awk' 24 | _DFCMD: 'df -lhTx tmpfs' 25 | _RAUDITCMD: 'cat /var/log/audit/audit.log' 26 | _UAUDITCMD: 'cat /var/log/kern.log' 27 | _JOURNALCMD: 'journalctl -b' 28 | 29 | CONTAINER: "false" 30 | 31 | gcp_credentials: ENCRYPTED[069aa0c73f34f33fde83379af6290e67f316115e18f4ee698743323f272220f445895487ed21a1db5173eb1a8d8f84f0] 32 | 33 | # Default timeout for each task 34 | timeout_in: 120m 35 | 36 | # Default VM to use unless set or modified by task 37 | gce_instance: 38 | image_project: "${IMAGE_PROJECT}" 39 | zone: "us-central1-c" # Required by Cirrus for the time being 40 | cpu: 2 41 | memory: "4Gb" 42 | disk: 200 # Gigabytes, do not set less than 200 per obscure GCE docs re: I/O performance 43 | image_name: "${FEDORA_CACHE_IMAGE_NAME}" 44 | 45 | 46 | testing_task: 47 | gce_instance: # Only need to specify differences from defaults (above) 48 | matrix: # Duplicate this task for each matrix product. 49 | image_name: "${FEDORA_CACHE_IMAGE_NAME}" 50 | 51 | # Separate scripts for separate outputs, makes debugging easier. 52 | setup_script: '${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/setup.sh |& ${_TIMESTAMP}' 53 | build_and_test_script: '${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/build_and_test.sh |& ${_TIMESTAMP}' 54 | 55 | # Log collection when job was successful 56 | df_script: '${_DFCMD} || true' 57 | rh_audit_log_script: '${_RAUDITCMD} || true' 58 | ubuntu_audit_log_script: '${_UAUDITCMD} || true' 59 | journal_log_script: '${_JOURNALCMD} || true' 60 | 61 | on_failure: # Script names must be different from above 62 | failure_df_script: '${_DFCMD} || true' 63 | failure_rh_audit_log_script: '${_RAUDITCMD} || true' 64 | failure_ubuntu_audit_log_script: '${_UAUDITCMD} || true' 65 | failure_journal_log_script: '${_JOURNALCMD} || true' 66 | 67 | 68 | # Update metadata on VM images referenced by this repository state 69 | meta_task: 70 | 71 | container: 72 | image: "quay.io/libpod/imgts:latest" # see contrib/imgts 73 | cpu: 1 74 | memory: 1 75 | 76 | env: 77 | # Space-separated list of images used by this repository state 78 | IMGNAMES: "${FEDORA_CACHE_IMAGE_NAME}" 79 | BUILDID: "${CIRRUS_BUILD_ID}" 80 | REPOREF: "${CIRRUS_CHANGE_IN_REPO}" 81 | GCPJSON: ENCRYPTED[0fa136ed0ef97bfa2e3e9e5e6316f7b41b93c6f70f4fc5d3002b73243666bd5d7e03a5fb3797562d720cded42fa4ca21] 82 | GCPNAME: ENCRYPTED[682b2bbe5929ff4118b9bcaf3348f001c10eacf25557ee4dbccf8158f7494f81aafa5137e268fb54aace30acc227744b] 83 | GCPPROJECT: ${IMAGE_PROJECT} 84 | CIRRUS_CLONE_DEPTH: 1 # source not used 85 | 86 | script: '/usr/local/bin/entrypoint.sh |& ${_TIMESTAMP}' 87 | -------------------------------------------------------------------------------- /contrib/cirrus/lib.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Library of common, shared utility functions. This file is intended 4 | # to be sourced by other scripts, not called directly. 5 | 6 | # Global details persist here 7 | source /etc/environment # not always loaded under all circumstances 8 | 9 | # Under some contexts these values are not set, make sure they are. 10 | export USER="$(whoami)" 11 | export HOME="$(getent passwd $USER | cut -d : -f 6)" 12 | [[ -n "$UID" ]] || export UID=$(getent passwd $USER | cut -d : -f 3) 13 | export GID=$(getent passwd $USER | cut -d : -f 4) 14 | 15 | # Essential default paths, many are overriden when executing under Cirrus-CI 16 | # others are duplicated here, to assist in debugging. 17 | export GOPATH="${GOPATH:-/var/tmp/go}" 18 | if type -P go &> /dev/null 19 | then 20 | # required for go 1.12+ 21 | export GOCACHE="${GOCACHE:-$HOME/.cache/go-build}" 22 | eval "$(go env)" 23 | # required by make and other tools 24 | export $(go env | cut -d '=' -f 1) 25 | # Ensure compiled tooling is reachable 26 | export PATH="$PATH:$GOPATH/bin" 27 | fi 28 | CIRRUS_WORKING_DIR="${CIRRUS_WORKING_DIR:-$GOPATH/src/github.com/containers/buildah}" 29 | export GOSRC="${GOSRC:-$CIRRUS_WORKING_DIR}" 30 | export PATH="$HOME/bin:$GOPATH/bin:/usr/local/bin:$PATH" 31 | SCRIPT_BASE=${GOSRC}/contrib/cirrus 32 | 33 | cd $CIRRUS_WORKING_DIR 34 | if type -P git &> /dev/null 35 | then 36 | CIRRUS_CHANGE_IN_REPO=${CIRRUS_CHANGE_IN_REPO:-$(git show-ref --hash=8 HEAD || date +%s)} 37 | else # pick something unique and obviously not from Cirrus 38 | CIRRUS_CHANGE_IN_REPO=${CIRRUS_CHANGE_IN_REPO:-no_git_$(date +%s)} 39 | fi 40 | 41 | export CI="${CI:-false}" 42 | CIRRUS_CI="${CIRRUS_CI:-false}" 43 | CONTINUOUS_INTEGRATION="${CONTINUOUS_INTEGRATION:-false}" 44 | CIRRUS_REPO_NAME=${CIRRUS_REPO_NAME:-buildah} 45 | CIRRUS_BASE_SHA=${CIRRUS_BASE_SHA:-unknown$(date +%s)} # difficult to reliably discover 46 | CIRRUS_BUILD_ID=${CIRRUS_BUILD_ID:-$RANDOM$(date +%s)} # must be short and unique 47 | 48 | # Unsafe env. vars for display 49 | SECRET_ENV_RE='(IRCID)|(ACCOUNT)|(^GC[EP]..+)|(SSH)' 50 | 51 | # GCE image-name compatible string representation of distribution name 52 | OS_RELEASE_ID="$(source /etc/os-release; echo $ID)" 53 | # GCE image-name compatible string representation of distribution _major_ version 54 | OS_RELEASE_VER="$(source /etc/os-release; echo $VERSION_ID | cut -d '.' -f 1)" 55 | # Combined to ease soe usage 56 | OS_REL_VER="${OS_RELEASE_ID}-${OS_RELEASE_VER}" 57 | 58 | # Working with apt under Debian/Ubuntu automation is a PITA, make it easy 59 | # Avoid some ways of getting stuck waiting for user input 60 | export DEBIAN_FRONTEND=noninteractive 61 | # Short-cut for frequently used base command 62 | export APTGET='apt-get -qq --yes' 63 | # Short list of packages or quick-running command 64 | SHORT_APTGET="timeout_attempt_delay_command 24s 5 30s $APTGET" 65 | # Long list / long-running command 66 | LONG_APTGET="timeout_attempt_delay_command 300s 5 30s $APTGET" 67 | 68 | # Pass in a list of one or more envariable names; exit non-zero with 69 | # helpful error message if any value is empty 70 | req_env_var() { 71 | # Provide context. If invoked from function use its name; else script name 72 | local caller=${FUNCNAME[1]} 73 | if [[ -n "$caller" ]]; then 74 | # Indicate that it's a function name 75 | caller="$caller()" 76 | else 77 | # Not called from a function: use script name 78 | caller=$(basename $0) 79 | fi 80 | 81 | # Usage check 82 | [[ -n "$1" ]] || die 1 "FATAL: req_env_var: invoked without arguments" 83 | 84 | # Each input arg is an envariable name, e.g. HOME PATH etc. Expand each. 85 | # If any is empty, bail out and explain why. 86 | for i; do 87 | if [[ -z "${!i}" ]]; then 88 | die 9 "FATAL: $caller requires \$$i to be non-empty" 89 | fi 90 | done 91 | } 92 | 93 | show_env_vars() { 94 | echo "Showing selection of environment variable definitions:" 95 | _ENV_VAR_NAMES=$(awk 'BEGIN{for(v in ENVIRON) print v}' | \ 96 | egrep -v "(^PATH$)|(^BASH_FUNC)|(^[[:punct:][:space:]]+)|$SECRET_ENV_RE" | \ 97 | sort -u) 98 | for _env_var_name in $_ENV_VAR_NAMES 99 | do 100 | # Supports older BASH versions 101 | printf " ${_env_var_name}=%q\n" "$(printenv $_env_var_name)" 102 | done 103 | } 104 | 105 | die() { 106 | echo "************************************************" 107 | echo ">>>>> ${2:-FATAL ERROR (but no message given!) in ${FUNCNAME[1]}()}" 108 | echo "************************************************" 109 | exit ${1:-1} 110 | } 111 | 112 | bad_os_id_ver() { 113 | echo "Unknown/Unsupported distro. $OS_RELEASE_ID and/or version $OS_RELEASE_VER for $(basename $0)" 114 | exit 42 115 | } 116 | 117 | timeout_attempt_delay_command() { 118 | TIMEOUT=$1 119 | ATTEMPTS=$2 120 | DELAY=$3 121 | shift 3 122 | STDOUTERR=$(mktemp -p '' $(basename $0)_XXXXX) 123 | req_env_var ATTEMPTS DELAY 124 | echo "Retrying $ATTEMPTS times with a $DELAY delay, and $TIMEOUT timeout for command: $@" 125 | for (( COUNT=1 ; COUNT <= $ATTEMPTS ; COUNT++ )) 126 | do 127 | echo "##### (attempt #$COUNT)" &>> "$STDOUTERR" 128 | if timeout --foreground $TIMEOUT "$@" &>> "$STDOUTERR" 129 | then 130 | echo "##### (success after #$COUNT attempts)" &>> "$STDOUTERR" 131 | break 132 | else 133 | echo "##### (failed with exit: $?)" &>> "$STDOUTERR" 134 | sleep $DELAY 135 | fi 136 | done 137 | cat "$STDOUTERR" 138 | rm -f "$STDOUTERR" 139 | if (( COUNT > $ATTEMPTS )) 140 | then 141 | echo "##### (exceeded $ATTEMPTS attempts)" 142 | exit 125 143 | fi 144 | } 145 | 146 | # Helper/wrapper script to only show stderr/stdout on non-zero exit 147 | install_ooe() { 148 | req_env_var SCRIPT_BASE 149 | echo "Installing script to mask stdout/stderr unless non-zero exit." 150 | install -D -m 755 "$SCRIPT_BASE/ooe.sh" /usr/local/bin/ooe.sh 151 | } 152 | 153 | showrun() { 154 | if [[ "$1" == "--background" ]] 155 | then 156 | shift 157 | # Properly escape any nested spaces, so command can be copy-pasted 158 | echo '+ '$(printf " %q" "$@")' &' > /dev/stderr 159 | "$@" & 160 | echo -e "${RED}${NOR}" 161 | else 162 | echo '--------------------------------------------------' 163 | echo '+ '$(printf " %q" "$@") > /dev/stderr 164 | "$@" 165 | fi 166 | } 167 | -------------------------------------------------------------------------------- /layout.md: -------------------------------------------------------------------------------- 1 | # Layout of Source Image 2 | 3 | ## Overview 4 | 5 | This tool builds an [OCI Image](https://github.com/opencontainers/image-spec/blob/master/config.md) comprised of the sources that correspond to another "works" image. 6 | This source image can be pushed to a container registry that satisfies the [OCI Distribution API](https://github.com/opencontainers/distribution-spec). 7 | In this way the source code is available in equivalent access as the works (binaries). 8 | 9 | ## use case 10 | 11 | For the sake of this document the reference example will be building a source image from Source RPMs (SRPMs). 12 | 13 | The current command to build this is: 14 | ```shell 15 | $ ls SRCRPMS/*.src.rpm | wc -l 16 | 103 17 | $ ./BuildSourceImage.sh -o ./output -s ./SRCRPMS/ 18 | [SrcImg][INFO] calling source collection drivers 19 | [SrcImg][INFO] --> context_dir 20 | [SrcImg][INFO] --> extra_src_dir 21 | [SrcImg][INFO] --> rpm_dir 22 | [SrcImg][INFO] --> rpm_fetch 23 | [SrcImg][INFO] packed 'oci:/home/vbatts/src/github.com/containers/BuildSourceImage/SrcImg/tmp/SrcImg.z3HxHN:latest-source' 24 | [SrcImg][INFO] succesfully packed 'oci:/home/vbatts/src/github.com/containers/BuildSourceImage/SrcImg/tmp/SrcImg.z3HxHN:latest-source' 25 | [SrcImg][INFO] copied to oci:./output:latest-source 26 | ``` 27 | 28 | ## The Output Image 29 | 30 | From the example above, `oci:./output:latest-source` is where the source image is written to. 31 | This is an [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-layout.md). 32 | 33 | ```shell 34 | ./output 35 | ├── blobs 36 | │   └── sha256 37 | │   ├── 01db9482eb66aa679d84e1737f0fb3f97424b7e758c8dff0567bfafbcd13dca0 38 | [...] 39 | │   └── fc02f78fb2d75df57893716d9beed4ea86d3adec8ef5aa7cbf6a52fb24ac0a79 40 | ├── index.json 41 | └── oci-layout 42 | 43 | 2 directories, 107 files 44 | ``` 45 | 46 | ### image-index (manifest-list) 47 | 48 | At the root of this output is the `index.json` which an [OCI image-index](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). 49 | For our example, here are the [`jq` formatted] contents of `index.json`: 50 | 51 | ```json 52 | { 53 | "schemaVersion": 2, 54 | "manifests": [ 55 | { 56 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 57 | "digest": "sha256:60632c06e3ff9637fd5e5feb3f8768590fc94f9a701f99bc88a8f9e07c737b71", 58 | "size": 1935, 59 | "annotations": { 60 | "com.redhat.image.type": "source", 61 | "org.opencontainers.image.ref.name": "latest-source" 62 | } 63 | } 64 | ] 65 | } 66 | ``` 67 | 68 | The list of a single manifest points to digest `sha256:60632c06e3ff9637fd5e5feb3f8768590fc94f9a701f99bc88a8f9e07c737b71`. 69 | _Notice_: We are overloading the use of the mediaType `application/vnd.oci.image.manifest.v1+json` so that the nested list of "`layers`" objects are nicely handled by the endpoint registry's garbage-collection. 70 | Here we include "`annotations`" denoting both the image tag, as well as an arbitrary type to indicate the type of this image. 71 | Using a "type" in this way is a complement pattern to the "platform.os" of a runnable container image. 72 | 73 | ### manifest 74 | 75 | Using the digest pointed to in the image-index, is the [OCI manifest](https://github.com/opencontainers/image-spec/blob/v1.0.1/manifest.md) for this source image. 76 | Generally these manifest are geared for runnable container images, that will be fetched and composed from layers of file systems. 77 | For this first version of source container images, the layers are file systems, but that contain the source objects that a corresponding runnable image are comprised of. 78 | 79 | ```json 80 | { 81 | "schemaVersion": 2, 82 | "config": { 83 | "mediaType": "application/vnd.oci.image.config.v1+json", 84 | "digest": "sha256:6083b2ea7b049ca9cc1d4708c160aec7a145960207be3c7bea19bec7e46f1ed1", 85 | "size": 948 86 | }, 87 | "layers": [ 88 | { 89 | "mediaType": "application/vnd.oci.image.layer.v1.tar", 90 | "size": 20480, 91 | "digest": "sha256:f24d714aee6b35aa16ddd892d3b789678e703ca09fa64ecf966ca1c58a388c62", 92 | "annotations": { 93 | "source.artifact.filename": "basesystem-10.0-7.el7.centos.src.rpm", 94 | "source.artifact.name": "basesystem", 95 | "source.artifact.version": "10.0", 96 | "source.artifact.epoch": "10.0", 97 | "source.artifact.release": "7.el7.centos", 98 | "source.artifact.license": "Public Domain", 99 | "source.artifact.mimetype": "application/x-rpm", 100 | "source.artifact.pkgid": "d5194181f6f572552e89e2d721612492", 101 | "source.artifact.buildtime": "1403865430" 102 | } 103 | }, 104 | { 105 | "mediaType": "application/vnd.oci.image.layer.v1.tar", 106 | "size": 20480, 107 | "digest": "sha256:4b6016d4b66e9cfd6a3731e22ac3805b648ddd06e2b13c9530475409ee3e3514", 108 | "annotations": { 109 | "source.artifact.filename": "rootfiles-8.1-11.el7.src.rpm", 110 | "source.artifact.name": "rootfiles", 111 | "source.artifact.version": "8.1", 112 | "source.artifact.epoch": "8.1", 113 | "source.artifact.release": "11.el7", 114 | "source.artifact.license": "Public Domain", 115 | "source.artifact.mimetype": "application/x-rpm", 116 | "source.artifact.pkgid": "e1cca1fe49265b419a01a686194406ef", 117 | "source.artifact.buildtime": "1402344692" 118 | } 119 | }, 120 | [...] 121 | ``` 122 | 123 | Here we point to a "config" for the sake of compatibility, and have "layers" of the source image. 124 | Each layer is the sources of a component of the resulting image. 125 | Again, for the sake of compatibility with older clients, each layer has the source stored in a TAR archive. 126 | 127 | Since each item in the array of layers is an [OCI image descriptor](https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#properties), we utilize attaching annotations to each source object. 128 | These annotation keys are to give insight to the nature of the content in the referenced blob. 129 | Also, while there may be annotations specific to the mimetype of source i.e. `source.artifact.epoch` and `source.artifact.pkgid` are generally RPM specific, the keys are intended to be generic across source types. 130 | Obviously having these kinds of generic, comparable values is a known challenge. 131 | 132 | ### Config 133 | 134 | For this first version of source images, the "`config`" pointed to by the mediaType "`application/vnd.oci.image.config.v1+json`" is the most pointless, but is there for broadest reception by client tooling. 135 | This is where the environment variables and command entrypoints would exist, but we have none. 136 | Here is the [`jq` formatted] contents of that config blob: 137 | 138 | ```json 139 | { 140 | "created": "2020-02-06T14:02:23.746051982-05:00", 141 | "architecture": "amd64", 142 | "os": "linux", 143 | "config": {}, 144 | "rootfs": { 145 | "type": "layers", 146 | "diff_ids": [ 147 | "sha256:f24d714aee6b35aa16ddd892d3b789678e703ca09fa64ecf966ca1c58a388c62", 148 | "sha256:4b6016d4b66e9cfd6a3731e22ac3805b648ddd06e2b13c9530475409ee3e3514", 149 | "sha256:09ca0817168b2cbd93e06a1b31a44f611e308abd2591df3c8f9d5d633668de19" 150 | ] 151 | }, 152 | "history": [ 153 | { 154 | "created": "2020-02-06T14:02:23.180531458-05:00", 155 | "created_by": "#(nop) BuildSourceImage.sh version 0.2.0-dev adding artifact: 8731e2c6d61bdebe2ac2301fdd80ea3223830f2b37c04511a48820f3b8b68b55" 156 | }, 157 | { 158 | "created": "2020-02-06T14:02:23.466891360-05:00", 159 | "created_by": "#(nop) BuildSourceImage.sh version 0.2.0-dev adding artifact: 676c8563e990f5312fc3de24be00a955fe302eee47f06b3c0a1935d472d6073b" 160 | }, 161 | { 162 | "created": "2020-02-06T14:02:23.746051982-05:00", 163 | "created_by": "#(nop) BuildSourceImage.sh version 0.2.0-dev adding artifact: 708825c991ae0a190d0e28fed3ca3d0874d5d9fedd8d6ad83fb0c77a8636d11d" 164 | } 165 | ] 166 | } 167 | ``` 168 | 169 | Honestly, none of this current data structure should be used be any clients of source images. 170 | Though any clients that are not aware of source images, could still fetch these data structures, and not strictly fail to unknown types. 171 | 172 | #### Next generation config 173 | 174 | Ideally this "config" will be document like a software bill of materials (SBOM). 175 | Join the OCI weekly discussions, and follow the [OCI Artifacts](https://github.com/opencontainers/artifacts) for possibilities. 176 | 177 | ### Blobs 178 | 179 | In the first version of this source image approach, each of the "layer" blobs is a tar archive, as most container runtimes and registries expect. 180 | 181 | During this source image version that layers are TAR archives, the format of their contents are as follows: 182 | 183 | * a "blobs" top-level directory 184 | * the source object itself is stored in a [blob digest](https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#digests) directory structure 185 | * a source-collector top-level directory 186 | * the file name of source object, as a symlink pointing to the hashed blob 187 | 188 | ```shell 189 | $ file ./output/blobs/sha256/fc02f78fb2d75df57893716d9beed4ea86d3adec8ef5aa7cbf6a52fb24ac0a79 190 | ./output/blobs/sha256/fc02f78fb2d75df57893716d9beed4ea86d3adec8ef5aa7cbf6a52fb24ac0a79: gzip compressed data, original size 399360 191 | $ tar tvf ./output/blobs/sha256/fc02f78fb2d75df57893716d9beed4ea86d3adec8ef5aa7cbf6a52fb24ac0a79 192 | drwxrw-rw- root/root 0 1969-12-31 19:00 ./ 193 | drwxrwxrwx root/root 0 1969-12-31 19:00 ./blobs/ 194 | drwxrwxrwx root/root 0 1969-12-31 19:00 ./blobs/sha256/ 195 | -rw-rw-rw- root/root 392884 1969-12-31 19:00 ./blobs/sha256/f78861cf3acb8335b7c4c0fee9286f0f8e8743c64f6698a9f8de8f92e2f31454 196 | drwxrwxrwx root/root 0 1969-12-31 19:00 ./rpm_dir/ 197 | lrwxrwxrwx root/root 0 1969-12-31 19:00 ./rpm_dir/libverto-0.3.0-8.fc31.src.rpm -> ../blobs/sha256/f78861cf3acb8335b7c4c0fee9286f0f8e8743c64f6698a9f8de8f92e2f31454 198 | ``` 199 | 200 | Logic being: trying to eliminate chances of collision of objects when a collection of these source layers are unpacked together. 201 | 202 | 203 | ## Unpacked 204 | 205 | Unpacking these "layers" will create a folder of all the source artifacts that comprise the source image. 206 | 207 | ```shell 208 | $ ./BuildSourceImage.sh unpack ./output/ ./unpack 209 | [SrcImg][INFO] [unpacking] layer sha256:16644f25b9842037fcedcee7740f8bb5d56ab2045923924b14814a7a9ddb068b 210 | [SrcImg][INFO] [unpacking] layer sha256:3287455f170c017e664c0b78e3161fe586d835d774c562fe7f289972cd3df1da 211 | [...] 212 | [SrcImg][INFO] [unpacking] layer sha256:918c58b1e7feb07f04d9119b1f8faa0d0d9dd5d6f08221124ddd422a3c462f5d 213 | [SrcImg][INFO] [unpacking] layer sha256:e49c55a056bc351e5df5183e361d97bc5d224a802a42d81834c4cabb404f13d6 214 | ``` 215 | 216 | This destination directory that has been unpacked to will be in a nested `rootfs/` directory. 217 | A behavior inherited from `umoci`. 218 | 219 | Each of the "source collection drivers" makes a subdirectory for the artifacts it collects. 220 | 221 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /BuildSourceImage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script builds a Source Image via "drivers" to collect source 4 | 5 | export ABV_NAME="SrcImg" 6 | # TODO maybe a flag for this? 7 | export source_image_suffix="-source" 8 | 9 | # output version string 10 | _version() { 11 | echo "$(basename "${0}") version 0.2.0-dev" 12 | } 13 | 14 | # output the cli usage and exit 15 | _usage() { 16 | _version 17 | echo "Usage: $(basename "$0") [-D] [-b ] [-c ] [-e ] [-r ] [-o ] [-p ] [-l] [-d ]" 18 | echo "" 19 | echo " Container Source Image tool" 20 | echo "" 21 | echo -e " -b \tbase path for source image builds" 22 | echo -e " -c \tbuild context for the container image. Can be provided via CONTEXT_DIR env variable" 23 | echo -e " -e \textra src for the container image. Can be provided multiple times" 24 | echo -e " -s \tdirectory of SRPMS to add. Can be provided via SRPM_DIR env variable" 25 | echo -e " -o \toutput the OCI image to path. Can be provided via OUTPUT_DIR env variable" 26 | echo -e " -d \tenumerate specific source drivers to run" 27 | echo -e " -l\t\tlist the source drivers available" 28 | echo -e " -p \tpush source image to specified reference after build" 29 | echo -e " -D\t\tdebuging output. Can be set via DEBUG env variable" 30 | echo -e " -h\t\tthis usage information" 31 | echo -e " -v\t\tversion" 32 | echo -e "" 33 | echo -e " Subcommands:" 34 | echo -e " unpack\tUnpack an OCI layout to a rootfs directory" 35 | echo -e "" 36 | } 37 | 38 | # sanity checks on startup 39 | _init() { 40 | set -o pipefail 41 | 42 | # check for tools we depend on 43 | for cmd in jq skopeo file find tar stat date ; do 44 | if [ -z "$(command -v ${cmd})" ] ; then 45 | # TODO: maybe this could be individual checks so it can report 46 | # where to find the tools 47 | _error "please install package to provide '${cmd}'" 48 | fi 49 | done 50 | 51 | if [ -z "$(command -v dnf)" ] && [ -z "$(command -v yumdownloader)" ] ; then 52 | _error "please provide either 'dnf download' or 'yumdownloader'" 53 | fi 54 | } 55 | 56 | # enable access to some of functions as subcommands! 57 | _subcommand() { 58 | local command="${1}" 59 | local ret 60 | 61 | shift 62 | 63 | case "${command}" in 64 | unpack) 65 | # (vb) i'd prefer this subcommand directly match the function name, but it isn't as pretty. 66 | unpack_img "${@}" 67 | ret=$? 68 | exit "${ret}" 69 | ;; 70 | esac 71 | } 72 | 73 | # _is_sourced tests whether this script is being source, or executed directly 74 | _is_sourced() { 75 | # https://unix.stackexchange.com/a/215279 76 | # thanks @tianon 77 | [ "${FUNCNAME[${#FUNCNAME[@]} - 1]}" == 'source' ] 78 | } 79 | 80 | # count $character $string 81 | _count_char_in_string() { 82 | c="${2//[^${1}]}" 83 | echo -n ${#c} 84 | } 85 | 86 | # size of file/directory in bytes 87 | _size() { 88 | du -b "${1}" | awk '{ ORS=""; print $1 }' 89 | } 90 | 91 | # date timestamp in RFC 3339, to the nanosecond, but slightly golang style ... 92 | _date_ns() { 93 | date --rfc-3339=ns | tr ' ' 'T' | tr -d '\n' 94 | } 95 | 96 | # local `mktemp -d` 97 | _mktemp_d() { 98 | local v 99 | v=$(mktemp -d "${TMPDIR:-/tmp}/${ABV_NAME}.XXXXXX") 100 | _debug "mktemp -d --> ${v}" 101 | echo "${v}" 102 | } 103 | 104 | # local `mktemp` 105 | _mktemp() { 106 | local v 107 | v=$(mktemp "${TMPDIR:-/tmp}/${ABV_NAME}.XXXXXX") 108 | _debug "mktemp --> ${v}" 109 | echo "${v}" 110 | } 111 | 112 | # local rm -rf 113 | _rm_rf() { 114 | _debug "rm -rf ${*}" 115 | rm -rf "${@}" 116 | } 117 | 118 | # local mkdir -p 119 | _mkdir_p() { 120 | if [ -n "${DEBUG}" ] ; then 121 | mkdir -vp "${@}" 122 | else 123 | mkdir -p "${@}" 124 | fi 125 | } 126 | 127 | # local tar 128 | _tar() { 129 | if [ -n "${DEBUG}" ] ; then 130 | tar -v "${@}" 131 | else 132 | tar "${@}" 133 | fi 134 | } 135 | 136 | _rpm_download() { 137 | if [ "$(command -v yumdownloader)" != "" ] ; then 138 | yumdownloader "${@}" 139 | else 140 | dnf download "${@}" 141 | fi 142 | } 143 | 144 | # output things, only when $DEBUG is set 145 | _debug() { 146 | if [ -n "${DEBUG}" ] ; then 147 | echo "[${ABV_NAME}][DEBUG] ${*}" >&2 148 | fi 149 | } 150 | 151 | # general echo but with prefix 152 | _info() { 153 | echo "[${ABV_NAME}][INFO] ${*}" 154 | } 155 | 156 | _warn() { 157 | echo "[${ABV_NAME}][WARN] ${*}" >&2 158 | } 159 | 160 | # general echo but with prefix 161 | _error() { 162 | echo "[${ABV_NAME}][ERROR] ${*}" >&2 163 | exit 1 164 | } 165 | 166 | # 167 | # parse the OCI image reference, accounting for: 168 | # * transport name 169 | # * presence or lack of transport port number 170 | # * presence or lack of digest 171 | # * presence or lack of image tag 172 | # 173 | 174 | # 175 | # return the image reference's digest, if any 176 | # 177 | parse_img_digest() { 178 | local ref="${1}" 179 | local digest="" 180 | if [ "$(_count_char_in_string '@' "${ref}")" -gt 0 ] ; then 181 | digest="${ref##*@}" # the digest after the "@" 182 | fi 183 | echo -n "${digest}" 184 | } 185 | 186 | # 187 | # determine image base name (without tag or digest) 188 | # 189 | parse_img_base() { 190 | local ref="${1%@*}" # just the portion before the digest "@" 191 | local base="${ref}" # default base is their reference 192 | local last_word="" # splitting up their reference to get the last word/chunk 193 | last_word="$(echo "${ref}" | tr '/' '\n' | tail -1 )" 194 | if [ "$(_count_char_in_string ':' "${last_word}")" -gt 0 ] ; then 195 | # which means everything before it is the base image name, **including 196 | # transport (which could have a port delineation), and even a URI like network ports. 197 | base="$(echo "${ref}" | rev | cut -d : -f 2 | rev )" 198 | fi 199 | echo -n "${base}" 200 | } 201 | 202 | # 203 | # determine, or guess, the image tag from the provided image reference 204 | # 205 | parse_img_tag() { 206 | local ref="${1%@*}" # just the portion before the digest "@" 207 | local tag="latest" # default tag 208 | 209 | if [ -z "${ref}" ] ; then 210 | echo -n "${tag}" 211 | return 0 212 | fi 213 | 214 | local last_word="" # splitting up their reference to get the last word/chunk 215 | last_word="$(echo "${ref}" | tr '/' '\n' | tail -1 )" 216 | if [ "$(_count_char_in_string ':' "${last_word}")" -gt 0 ] ; then 217 | # if there are colons in the last segment after '/', then get that tag name 218 | tag="${last_word#*:}" # this parameter expansion removes the prefix pattern before the ':' 219 | fi 220 | echo -n "${tag}" 221 | } 222 | 223 | # 224 | # an inline prefixer for containers/image tools 225 | # 226 | ref_prefix() { 227 | local ref="${1}" 228 | local pfxs 229 | local ret 230 | 231 | # get the supported prefixes of the current version of skopeo 232 | mapfile -t pfxs < <(skopeo copy --help | grep -A1 "Supported transports:" | grep -v "Supported transports" | sed 's/, /\n/g') 233 | ret=$? 234 | if [ ${ret} -ne 0 ] ; then 235 | return ${ret} 236 | fi 237 | 238 | for pfx in "${pfxs[@]}" ; do 239 | if echo "${ref}" | grep -q "^${pfx}:" ; then 240 | # break if we match a known prefix 241 | echo "${ref}" 242 | return 0 243 | fi 244 | done 245 | # else default 246 | echo "docker://${ref}" 247 | } 248 | 249 | # 250 | # an inline namer for the source image 251 | # Initially this is a tagging convention (which if we try estesp/manifest-tool 252 | # can be directly mapped into a manifest-list/image-index). 253 | # 254 | ref_src_img_tag() { 255 | local ref="${1}" 256 | echo -n "$(parse_img_tag "${ref}")""${source_image_suffix}" 257 | } 258 | 259 | # 260 | # call out to registry for the image reference's digest checksum 261 | # 262 | fetch_img_digest() { 263 | local ref="${1}" 264 | local dgst 265 | local ret 266 | 267 | ## TODO: check for authfile, creds, and whether it's an insecure registry 268 | dgst=$(skopeo inspect "$(ref_prefix "${ref}")" | jq .Digest | tr -d \") 269 | ret=$? 270 | if [ $ret -ne 0 ] ; then 271 | echo "ERROR: check the image reference: ${ref}" >&2 272 | return $ret 273 | fi 274 | 275 | echo -n "${dgst}" 276 | } 277 | 278 | # 279 | # pull down the image to an OCI layout 280 | # arguments: image ref 281 | # returns: path:tag to the OCI layout 282 | # 283 | # any commands should only output to stderr, so that the caller can receive the 284 | # path reference to the OCI layout. 285 | # 286 | fetch_img() { 287 | local ref="${1}" 288 | local dst="${2}" 289 | local base 290 | local tag 291 | local dgst 292 | local from 293 | local ret 294 | 295 | _mkdir_p "${dst}" 296 | 297 | base="$(parse_img_base "${ref}")" 298 | tag="$(parse_img_tag "${ref}")" 299 | dgst="$(parse_img_digest "${ref}")" 300 | from="" 301 | # skopeo currently only support _either_ tag _or_ digest, so we'll be specific. 302 | if [ -n "${dgst}" ] ; then 303 | from="$(ref_prefix "${base}")@${dgst}" 304 | else 305 | from="$(ref_prefix "${base}"):${tag}" 306 | fi 307 | 308 | ## TODO: check for authfile, creds, and whether it's an insecure registry 309 | ## destination name must have the image tag included (umoci expects it) 310 | skopeo \ 311 | copy \ 312 | "${from}" \ 313 | "oci:${dst}:${tag}" >&2 314 | ret=$? 315 | if [ ${ret} -ne 0 ] ; then 316 | return ${ret} 317 | fi 318 | echo -n "${dst}:${tag}" 319 | } 320 | 321 | # 322 | # upack_img 323 | # 324 | unpack_img() { 325 | local image_dir="${1}" 326 | local unpack_dir="${2}" 327 | local ret 328 | 329 | while getopts ":h" opts; do 330 | case "${opts}" in 331 | *) 332 | echo "$0 unpack " 333 | return 1 334 | ;; 335 | esac 336 | done 337 | shift $((OPTIND-1)) 338 | 339 | if [ -z "${image_dir}" ] || [ -z "${unpack_dir}" ] ; then 340 | _error "[unpack_img] blank arguments provided" 341 | fi 342 | 343 | if [ -d "${unpack_dir}" ] ; then 344 | _rm_rf "${unpack_dir}" 345 | fi 346 | 347 | if [ -n "$(command -v umoci)" ] ; then 348 | # can be done as non-root (even in a non-root container) 349 | unpack_img_umoci "${image_dir}" "${unpack_dir}" 350 | ret=$? 351 | if [ ${ret} -ne 0 ] ; then 352 | return ${ret} 353 | fi 354 | else 355 | # can be done as non-root (even in a non-root container) 356 | unpack_img_bash "${image_dir}" "${unpack_dir}" 357 | ret=$? 358 | if [ ${ret} -ne 0 ] ; then 359 | return ${ret} 360 | fi 361 | fi 362 | } 363 | 364 | # 365 | # unpack an image layout using only jq and bash 366 | # 367 | unpack_img_bash() { 368 | local image_dir="${1}" 369 | local unpack_dir="${2}" 370 | local mnfst_dgst 371 | local layer_dgsts 372 | local ret 373 | 374 | _debug "unpacking with bash+jq" 375 | 376 | # for compat with umoci (which wants the image tag as well) 377 | if echo "${image_dir}" | grep -q ":" ; then 378 | image_dir="${image_dir%:*}" 379 | fi 380 | 381 | mnfst_dgst="$(jq '.manifests[0].digest' "${image_dir}"/index.json | tr -d \")" 382 | ret=$? 383 | if [ ${ret} -ne 0 ] ; then 384 | return ${ret} 385 | fi 386 | 387 | # TODO this will need to be refactored when we start seeing +zstd layers. 388 | # Then it will be better to no just get a list of digests, but maybe to 389 | # iterate on each descriptor independently? 390 | layer_dgsts="$(jq '.layers | map(select(.mediaType == "application/vnd.oci.image.layer.v1.tar+gzip"),select(.mediaType == "application/vnd.oci.image.layer.v1.tar"),select(.mediaType == "application/vnd.docker.image.rootfs.diff.tar.gzip")) | .[] | .digest' "${image_dir}"/blobs/"${mnfst_dgst/://}" | tr -d \")" 391 | ret=$? 392 | if [ ${ret} -ne 0 ] ; then 393 | return ${ret} 394 | fi 395 | 396 | _mkdir_p "${unpack_dir}/rootfs" 397 | for dgst in ${layer_dgsts} ; do 398 | path="${image_dir}/blobs/${dgst/://}" 399 | tmp_file=$(_mktemp) 400 | zcat "${path}" | _tar -t > "$tmp_file" 401 | 402 | # look for '.wh.' entries. They must be removed from the rootfs 403 | # _before_ extracting the archive, then the .wh. entries themselves 404 | # need to not remain afterwards 405 | grep '\.wh\.' "${tmp_file}" | while read -r wh_path ; do 406 | # if `some/path/.wh.foo` then `rm -rf `${unpack_dir}/some/path/foo` 407 | # if `some/path/.wh..wh..opq` then `rm -rf `${unpack_dir}/some/path/*` 408 | if [ "$(basename "${wh_path}")" == ".wh..wh..opq" ] ; then 409 | _rm_rf "${unpack_dir}/rootfs/$(dirname "${wh_path}")/*" 410 | elif basename "${wh_path}" | grep -qe '^\.wh\.' ; then 411 | name=$(basename "${wh_path}" | sed -e 's/^\.wh\.//') 412 | _rm_rf "${unpack_dir}/rootfs/$(dirname "${wh_path}")/${name}" 413 | fi 414 | done 415 | 416 | _info "[unpacking] layer ${dgst}" 417 | # unpack layer to rootfs (without whiteouts) 418 | zcat "${path}" | _tar --restrict --no-xattr --no-acls --no-selinux --exclude='*.wh.*' -x -C "${unpack_dir}/rootfs" 419 | ret=$? 420 | if [ ${ret} -ne 0 ] ; then 421 | return ${ret} 422 | fi 423 | 424 | # some of the directories get unpacked as 0555, so removing them gives an EPERM 425 | find "${unpack_dir}" -type d -exec chmod 0755 "{}" \; 426 | done 427 | } 428 | 429 | # 430 | # unpack using umoci 431 | # 432 | unpack_img_umoci() { 433 | local image_dir="${1}" 434 | local unpack_dir="${2}" 435 | 436 | _debug "unpacking with umoci" 437 | # always assume we're not root I reckon 438 | umoci unpack --rootless --image "${image_dir}" "${unpack_dir}" >&2 439 | ret=$? 440 | return $ret 441 | } 442 | 443 | # 444 | # copy an image from one location to another 445 | # 446 | push_img() { 447 | local src="${1}" 448 | local dst="${2}" 449 | 450 | _debug "pushing image ${src} to ${dst}" 451 | ## TODO: check for authfile, creds, and whether it's an insecure registry 452 | skopeo copy --quiet --dest-tls-verify=false "$(ref_prefix "${src}")" "$(ref_prefix "${dst}")" # XXX for demo only 453 | #skopeo copy "$(ref_prefix "${src}")" "$(ref_prefix "${dst}")" 454 | ret=$? 455 | return $ret 456 | } 457 | 458 | # 459 | # sets up a basic new OCI layout, for an image with the provided (or default 'latest') tag 460 | # 461 | layout_new() { 462 | local out_dir="${1}" 463 | local image_tag="${2:-latest}" 464 | local ret 465 | 466 | if [ -n "$(command -v umoci)" ] ; then 467 | layout_new_umoci "${out_dir}" "${image_tag}" 468 | ret=$? 469 | if [ ${ret} -ne 0 ] ; then 470 | return ${ret} 471 | fi 472 | else 473 | layout_new_bash "${out_dir}" "${image_tag}" 474 | ret=$? 475 | if [ ${ret} -ne 0 ] ; then 476 | return ${ret} 477 | fi 478 | fi 479 | } 480 | 481 | # 482 | # sets up new OCI layout, using `umoci` 483 | # 484 | layout_new_umoci() { 485 | local out_dir="${1}" 486 | local image_tag="${2:-latest}" 487 | local ret 488 | 489 | # umoci expects the layout path to _not_ exist and will fail if it does exist 490 | _rm_rf "${out_dir}" 491 | 492 | umoci init --layout "${out_dir}" 493 | ret=$? 494 | if [ "${ret}" -ne 0 ] ; then 495 | return "${ret}" 496 | fi 497 | 498 | # XXX currently does not support adding the rich annotations like I've done with the _bash 499 | # https://github.com/openSUSE/umoci/issues/298 500 | umoci new --image "${out_dir}:${image_tag}" 501 | ret=$? 502 | if [ "${ret}" -ne 0 ] ; then 503 | return "${ret}" 504 | fi 505 | } 506 | 507 | # 508 | # sets up new OCI layout, all with bash and jq 509 | # 510 | layout_new_bash() { 511 | local out_dir="${1}" 512 | local image_tag="${2:-latest}" 513 | local config 514 | local mnfst 515 | local config_sum 516 | local mnfst_sum 517 | local ret 518 | 519 | _mkdir_p "${out_dir}/blobs/sha256" 520 | echo '{"imageLayoutVersion":"1.0.0"}' > "${out_dir}/oci-layout" 521 | config=' 522 | { 523 | "created": "'$(_date_ns)'", 524 | "architecture": "amd64", 525 | "os": "linux", 526 | "config": {}, 527 | "rootfs": { 528 | "type": "layers", 529 | "diff_ids": [] 530 | } 531 | } 532 | ' 533 | config_sum=$(echo "${config}" | jq -c | tr -d '\n' | sha256sum | awk '{ ORS=""; print $1 }') 534 | ret=$? 535 | if [ "${ret}" -ne 0 ] ; then 536 | return "${ret}" 537 | fi 538 | echo "${config}" | jq -c | tr -d '\n' > "${out_dir}/blobs/sha256/${config_sum}" 539 | ret=$? 540 | if [ "${ret}" -ne 0 ] ; then 541 | return "${ret}" 542 | fi 543 | 544 | mnfst=' 545 | { 546 | "schemaVersion": 2, 547 | "config": { 548 | "mediaType": "application/vnd.oci.image.config.v1+json", 549 | "digest": "sha256:'"${config_sum}"'", 550 | "size": '"$(_size "${out_dir}"/blobs/sha256/"${config_sum}")"' 551 | }, 552 | "layers": [] 553 | } 554 | ' 555 | mnfst_sum=$(echo "${mnfst}" | jq -c | tr -d '\n' | sha256sum | awk '{ ORS=""; print $1 }') 556 | echo "${mnfst}" | jq -c | tr -d '\n' > "${out_dir}/blobs/sha256/${mnfst_sum}" 557 | 558 | echo ' 559 | { 560 | "schemaVersion": 2, 561 | "manifests": [ 562 | { 563 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 564 | "digest": "sha256:'"${mnfst_sum}"'", 565 | "size": '"$(_size "${out_dir}"/blobs/sha256/"${mnfst_sum}")"', 566 | "annotations": { 567 | "org.opencontainers.image.ref.name": "'"${image_tag}"'" 568 | } 569 | } 570 | ] 571 | } 572 | ' | jq -c | tr -d '\n' > "${out_dir}/index.json" 573 | } 574 | 575 | # call this for every artifact, to insert it into an OCI layout 576 | # args: 577 | # * a path to the layout 578 | # * a path to the artifact 579 | # * the path inside the tar 580 | # * json file to slurp in as annotations for this layer's OCI descriptor 581 | # * tag used in the layout (default is 'latest') 582 | # 583 | layout_insert() { 584 | local out_dir="${1}" 585 | local artifact_path="${2}" 586 | local tar_path="${3}" 587 | local annotations_file="${4}" 588 | local image_tag="${5:-latest}" 589 | local ret 590 | 591 | if [ -n "$(command -v umoci)" ] ; then 592 | layout_insert_umoci "${out_dir}" "${artifact_path}" "${tar_path}" "${annotations_file}" "${image_tag}" 593 | ret=$? 594 | if [ ${ret} -ne 0 ] ; then 595 | return ${ret} 596 | fi 597 | else 598 | layout_insert_bash "${out_dir}" "${artifact_path}" "${tar_path}" "${annotations_file}" "${image_tag}" 599 | ret=$? 600 | if [ ${ret} -ne 0 ] ; then 601 | return ${ret} 602 | fi 603 | fi 604 | } 605 | 606 | layout_insert_umoci() { 607 | local out_dir="${1}" 608 | local artifact_path="${2}" 609 | local tar_path="${3}" 610 | local annotations_file="${4}" 611 | local image_tag="${5:-latest}" 612 | local sum 613 | local ret 614 | 615 | # prep the blob path for inside the layer, so we can just copy that whole path in 616 | tmpdir="$(_mktemp_d)" 617 | 618 | # TODO account for "artifact_path" being a directory? 619 | sum="$(sha256sum "${artifact_path}" | awk '{ print $1 }')" 620 | 621 | _mkdir_p "${tmpdir}/blobs/sha256" 622 | cp "${artifact_path}" "${tmpdir}/blobs/sha256/${sum}" 623 | if [ "$(basename "${tar_path}")" == "$(basename "${artifact_path}")" ] ; then 624 | _mkdir_p "${tmpdir}/$(dirname "${tar_path}")" 625 | # TODO this symlink need to be relative path, not to `/blobs/...` 626 | ln -s "/blobs/sha256/${sum}" "${tmpdir}/${tar_path}" 627 | else 628 | _mkdir_p "${tmpdir}/${tar_path}" 629 | # TODO this symlink need to be relative path, not to `/blobs/...` 630 | ln -s "/blobs/sha256/${sum}" "${tmpdir}/${tar_path}/$(basename "${artifact_path}")" 631 | fi 632 | 633 | # XXX currently does not support adding the rich annotations like I've done with the _bash 634 | # https://github.com/openSUSE/umoci/issues/298 635 | # XXX this insert operation can not disable compression 636 | # https://github.com/openSUSE/umoci/issues/300 637 | umoci insert \ 638 | --rootless \ 639 | --image "${out_dir}:${image_tag}" \ 640 | --history.created "$(_date_ns)" \ 641 | --history.comment "#(nop) $(_version) adding artifact: ${sum}" \ 642 | "${tmpdir}" "/" 643 | ret=$? 644 | if [ ${ret} -ne 0 ] ; then 645 | return ${ret} 646 | fi 647 | } 648 | 649 | layout_insert_bash() { 650 | local out_dir="${1}" 651 | local artifact_path="${2}" 652 | local tar_path="${3}" 653 | local annotations_file="${4}" 654 | local image_tag="${5:-latest}" 655 | local mnfst_list 656 | local mnfst_dgst 657 | local mnfst 658 | local tmpdir 659 | local sum 660 | local tmptar 661 | local tmptar_sum 662 | local tmptar_size 663 | local config_sum 664 | local tmpconfig 665 | local tmpconfig_sum 666 | local tmpconfig_size 667 | local tmpmnfst 668 | local tmpmnfst_sum 669 | local tmpmnfst_size 670 | local tmpmnfst_list 671 | 672 | mnfst_list="${out_dir}/index.json" 673 | # get the digest to the manifest 674 | test -f "${mnfst_list}" || return 1 675 | mnfst_dgst="$(jq --arg tag "${image_tag}" ' 676 | .manifests[] 677 | | select(.annotations."org.opencontainers.image.ref.name" == $tag ) 678 | | .digest 679 | ' "${mnfst_list}" | tr -d \" | tr -d '\n' )" 680 | mnfst="${out_dir}/blobs/${mnfst_dgst/://}" 681 | test -f "${mnfst}" || return 1 682 | 683 | # make tar of new object 684 | tmpdir="$(_mktemp_d)" 685 | # TODO account for "artifact_path" being a directory? 686 | sum="$(sha256sum "${artifact_path}" | awk '{ print $1 }')" 687 | # making a blob store in the layer 688 | _mkdir_p "${tmpdir}/blobs/sha256" 689 | cp "${artifact_path}" "${tmpdir}/blobs/sha256/${sum}" 690 | if [ "$(basename "${tar_path}")" == "$(basename "${artifact_path}")" ] ; then 691 | _mkdir_p "${tmpdir}/$(dirname "${tar_path}")" 692 | # TODO this symlink need to be relative path, not to `/blobs/...` 693 | ln -s "../blobs/sha256/${sum}" "${tmpdir}/${tar_path}" 694 | else 695 | _mkdir_p "${tmpdir}/${tar_path}" 696 | # TODO this symlink need to be relative path, not to `/blobs/...` 697 | ln -s "../blobs/sha256/${sum}" "${tmpdir}/${tar_path}/$(basename "${artifact_path}")" 698 | fi 699 | tmptar="$(_mktemp)" 700 | 701 | # zero all the things for as consistent blobs as possible 702 | _tar -C "${tmpdir}" --sort=name --mtime=@0 --owner=0 --group=0 --mode='a+rw' --no-xattrs --no-selinux --no-acls -cf "${tmptar}" . 703 | _rm_rf "${tmpdir}" 704 | 705 | # checksum tar and move to blobs/sha256/$checksum 706 | tmptar_sum="$(sha256sum "${tmptar}" | awk '{ ORS=""; print $1 }')" 707 | tmptar_size="$(_size "${tmptar}")" 708 | mv "${tmptar}" "${out_dir}/blobs/sha256/${tmptar_sum}" 709 | 710 | # find and read the prior config, mapped from the manifest 711 | config_sum="$(jq '.config.digest' "${mnfst}" | tr -d \")" 712 | 713 | # use `jq` to append to prior config 714 | tmpconfig="$(_mktemp)" 715 | jq -c \ 716 | --arg date "$(_date_ns)" \ 717 | --arg tmptar_sum "sha256:${tmptar_sum}" \ 718 | --arg comment "#(nop) $(_version) adding artifact: ${sum}" \ 719 | ' 720 | .created = $date 721 | | .rootfs.diff_ids += [ $tmptar_sum ] 722 | | .history += [ 723 | { 724 | "created": $date, 725 | "created_by": $comment 726 | } 727 | ] 728 | ' "${out_dir}/blobs/${config_sum/://}" > "${tmpconfig}" 729 | _rm_rf "${out_dir}/blobs/${config_sum/://}" 730 | 731 | # rename the config blob to its new checksum 732 | tmpconfig_sum="$(sha256sum "${tmpconfig}" | awk '{ ORS=""; print $1 }')" 733 | tmpconfig_size="$(_size "${tmpconfig}")" 734 | mv "${tmpconfig}" "${out_dir}/blobs/sha256/${tmpconfig_sum}" 735 | 736 | # append layers list in the manifest, and its new config mapping 737 | tmpmnfst="$(_mktemp)" 738 | jq -c \ 739 | --arg tmpconfig_sum "sha256:${tmpconfig_sum}" \ 740 | --arg tmpconfig_size "${tmpconfig_size}" \ 741 | --arg tmptar_sum "sha256:${tmptar_sum}" \ 742 | --arg tmptar_size "${tmptar_size}" \ 743 | --arg sum "sha256:${sum}" \ 744 | --slurpfile annotations_slurp "${annotations_file}" \ 745 | ' 746 | .config.digest = $tmpconfig_sum 747 | | .config.size = ($tmpconfig_size|tonumber) 748 | | ( { 749 | "source.artifact.filename.checksum": $sum 750 | } + $annotations_slurp[0] ) as $annotations_merge 751 | | .layers += [ 752 | { 753 | "mediaType": "application/vnd.oci.image.layer.v1.tar", 754 | "size": ($tmptar_size|tonumber), 755 | "digest": $tmptar_sum, 756 | "annotations": $annotations_merge 757 | } 758 | ] 759 | ' "${mnfst}" > "${tmpmnfst}" 760 | ret=$? 761 | if [ $ret -ne 0 ] ; then 762 | return 1 763 | fi 764 | _rm_rf "${mnfst}" 765 | 766 | # rename the manifest blob to its new checksum 767 | tmpmnfst_sum="$(sha256sum "${tmpmnfst}" | awk '{ ORS=""; print $1 }')" 768 | tmpmnfst_size="$(_size "${tmpmnfst}")" 769 | mv "${tmpmnfst}" "${out_dir}/blobs/sha256/${tmpmnfst_sum}" 770 | _debug "updated ${out_dir}/blobs/sha256/${tmpmnfst_sum}" 771 | 772 | # map the mnfst_list to the new mnfst checksum 773 | tmpmnfst_list="$(_mktemp)" 774 | jq -c \ 775 | --arg tag "${image_tag}" \ 776 | --arg tmpmnfst_sum "sha256:${tmpmnfst_sum}" \ 777 | --arg tmpmnfst_size "${tmpmnfst_size}" \ 778 | ' 779 | [(.manifests[] | select(.annotations."org.opencontainers.image.ref.name" != $tag) )] as $manifests_reduced 780 | | [ 781 | { 782 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 783 | "digest": $tmpmnfst_sum, 784 | "size": ($tmpmnfst_size|tonumber), 785 | "annotations": { 786 | "com.redhat.image.type": "source", 787 | "org.opencontainers.image.ref.name": $tag 788 | } 789 | } 790 | ] as $manifests_new 791 | | .manifests = $manifests_reduced + $manifests_new 792 | ' "${mnfst_list}" > "${tmpmnfst_list}" 793 | ret=$? 794 | if [ $ret -ne 0 ] ; then 795 | return 1 796 | fi 797 | mv "${tmpmnfst_list}" "${mnfst_list}" 798 | _debug "manifest-list updated ${mnfst_list}" 799 | } 800 | 801 | 802 | # 803 | # Source Collection Drivers 804 | # 805 | # presently just bash functions. *notice* prefix the function name as `sourcedriver_` 806 | # May become a ${ABV_NAME}/drivers.d/ 807 | # 808 | # Arguments: 809 | # * image ref 810 | # * path to inspect 811 | # * output path for source (specifc to this driver) 812 | # * output path for JSON file of source's annotations 813 | # 814 | # The JSON of source annotations is the key to discovering the source artifact 815 | # to be added and including rich metadata about that archive into the final 816 | # image. 817 | # The name of each JSON file is appending '.json' to the artifact's name. So if 818 | # you have `foo-1.0.src.rpm` then there MUST be a corresponding 819 | # `foo-1.0.src.rpm.json`. 820 | # The data structure in this annotation is just a dict/hashmap, with key/val 821 | # according to 822 | # https://github.com/opencontainers/image-spec/blob/master/annotations.md 823 | # 824 | 825 | # 826 | # driver to determine and fetch source rpms, based on the rootfs 827 | # 828 | sourcedriver_rpm_fetch() { 829 | local self="${0#sourcedriver_*}" 830 | local ref="${1}" 831 | local rootfs="${2}" 832 | local out_dir="${3}" 833 | local manifest_dir="${4}" 834 | local release 835 | local rpm 836 | local srcrpm_buildtime 837 | local srcrpm_pkgid 838 | local srcrpm_name 839 | local srcrpm_version 840 | local srcrpm_epoch 841 | local srcrpm_release 842 | local srcrpm_license 843 | local mimetype 844 | 845 | # Get the RELEASEVER from the image 846 | release=$(rpm -q --queryformat "%{VERSION}\n" --root "${rootfs}" -f /etc/os-release) 847 | 848 | # From the rootfs of the works image, build out the src rpms to operate over 849 | for srcrpm in $(rpm -qa --root "${rootfs}" --queryformat '%{SOURCERPM}\n' | grep -v '^gpg-pubkey' | sort -u) ; do 850 | if [ "${srcrpm}" == "(none)" ] ; then 851 | continue 852 | fi 853 | 854 | rpm=${srcrpm%*.src.rpm} 855 | if [ ! -f "${out_dir}/${srcrpm}" ] ; then 856 | _debug "--> fetching ${srcrpm}" 857 | _rpm_download \ 858 | --quiet \ 859 | --installroot "${rootfs}" \ 860 | --release "${release}" \ 861 | --destdir "${out_dir}" \ 862 | --source \ 863 | "${rpm}" 864 | ret=$? 865 | if [ $ret -ne 0 ] ; then 866 | _warn "failed to fetch ${srcrpm}" 867 | continue 868 | fi 869 | else 870 | _debug "--> using cached ${srcrpm}" 871 | fi 872 | 873 | # TODO one day, check and confirm with %{sourcepkgid} 874 | # https://bugzilla.redhat.com/show_bug.cgi?id=1741715 875 | #rpm_sourcepkgid=$(rpm -q --root ${rootfs} --queryformat '%{sourcepkgid}' "${rpm}") 876 | srcrpm_buildtime=$(rpm -qp --nosignature --qf '%{buildtime}' "${out_dir}"/"${srcrpm}" ) 877 | srcrpm_pkgid=$(rpm -qp --nosignature --qf '%{pkgid}' "${out_dir}"/"${srcrpm}" ) 878 | srcrpm_name=$(rpm -qp --nosignature --qf '%{name}' "${out_dir}"/"${srcrpm}" ) 879 | srcrpm_version=$(rpm -qp --nosignature --qf '%{version}' "${out_dir}"/"${srcrpm}" ) 880 | srcrpm_epoch=$(rpm -qp --nosignature --qf '%{epoch}' "${out_dir}"/"${srcrpm}" ) 881 | srcrpm_release=$(rpm -qp --nosignature --qf '%{release}' "${out_dir}"/"${srcrpm}" ) 882 | srcrpm_license=$(rpm -qp --nosignature --qf '%{license}' "${out_dir}"/"${srcrpm}" ) 883 | mimetype="$(file --brief --mime-type "${out_dir}"/"${srcrpm}")" 884 | jq \ 885 | -n \ 886 | --arg filename "${srcrpm}" \ 887 | --arg name "${srcrpm_name}" \ 888 | --arg version "${srcrpm_version}" \ 889 | --arg epoch "${srcrpm_epoch}" \ 890 | --arg release "${srcrpm_release}" \ 891 | --arg license "${srcrpm_license}" \ 892 | --arg buildtime "${srcrpm_buildtime}" \ 893 | --arg mimetype "${mimetype}" \ 894 | ' 895 | { 896 | "source.artifact.filename": $filename, 897 | "source.artifact.name": $name, 898 | "source.artifact.version": $version, 899 | "source.artifact.epoch": $epoch, 900 | "source.artifact.release": $release, 901 | "source.artifact.license": $license, 902 | "source.artifact.mimetype": $mimetype, 903 | "source.artifact.buildtime": $buildtime 904 | } 905 | ' \ 906 | > "${manifest_dir}/${srcrpm}.json" 907 | ret=$? 908 | if [ $ret -ne 0 ] ; then 909 | return 1 910 | fi 911 | done 912 | } 913 | 914 | # 915 | # driver to only package rpms from a provided rpm directory 916 | # (koji use-case) 917 | # 918 | sourcedriver_rpm_dir() { 919 | local self="${0#sourcedriver_*}" 920 | local ref="${1}" 921 | local rootfs="${2}" 922 | local out_dir="${3}" 923 | local manifest_dir="${4}" 924 | local srcrpm_buildtime 925 | local srcrpm_pkgid 926 | local srcrpm_name 927 | local srcrpm_version 928 | local srcrpm_epoch 929 | local srcrpm_release 930 | local srcrpm_license 931 | local mimetype 932 | 933 | if [ -n "${SRPM_DIR}" ]; then 934 | _debug "[$self] writing to $out_dir and $manifest_dir" 935 | find "${SRPM_DIR}" -type f -name '*src.rpm' | while read -r srcrpm ; do 936 | cp "${srcrpm}" "${out_dir}" 937 | srcrpm="$(basename "${srcrpm}")" 938 | _debug "[$self] --> ${srcrpm}" 939 | srcrpm_buildtime=$(rpm -qp --nosignature --qf '%{buildtime}' "${out_dir}"/"${srcrpm}" ) 940 | srcrpm_pkgid=$(rpm -qp --nosignature --qf '%{pkgid}' "${out_dir}"/"${srcrpm}" ) 941 | srcrpm_name=$(rpm -qp --nosignature --qf '%{name}' "${out_dir}"/"${srcrpm}" ) 942 | srcrpm_version=$(rpm -qp --nosignature --qf '%{version}' "${out_dir}"/"${srcrpm}" ) 943 | srcrpm_epoch=$(rpm -qp --nosignature --qf '%{epoch}' "${out_dir}"/"${srcrpm}" ) 944 | srcrpm_release=$(rpm -qp --nosignature --qf '%{release}' "${out_dir}"/"${srcrpm}" ) 945 | srcrpm_license=$(rpm -qp --nosignature --qf '%{license}' "${out_dir}"/"${srcrpm}" ) 946 | mimetype="$(file --brief --mime-type "${out_dir}"/"${srcrpm}")" 947 | jq \ 948 | -n \ 949 | --arg filename "${srcrpm}" \ 950 | --arg name "${srcrpm_name}" \ 951 | --arg version "${srcrpm_version}" \ 952 | --arg epoch "${srcrpm_epoch}" \ 953 | --arg release "${srcrpm_release}" \ 954 | --arg license "${srcrpm_license}" \ 955 | --arg buildtime "${srcrpm_buildtime}" \ 956 | --arg mimetype "${mimetype}" \ 957 | --arg pkgid "${srcrpm_pkgid}" \ 958 | ' 959 | { 960 | "source.artifact.filename": $filename, 961 | "source.artifact.name": $name, 962 | "source.artifact.version": $version, 963 | "source.artifact.epoch": $version, 964 | "source.artifact.release": $release, 965 | "source.artifact.license": $license, 966 | "source.artifact.mimetype": $mimetype, 967 | "source.artifact.pkgid": $pkgid, 968 | "source.artifact.buildtime": $buildtime 969 | } 970 | ' \ 971 | > "${manifest_dir}/${srcrpm}.json" 972 | ret=$? 973 | if [ $ret -ne 0 ] ; then 974 | return 1 975 | fi 976 | done 977 | fi 978 | } 979 | 980 | # 981 | # If the caller specified a context directory, 982 | # 983 | # slightly special driver, as it has a flag/env passed in, that it uses 984 | # 985 | sourcedriver_context_dir() { 986 | local self="${0#sourcedriver_*}" 987 | local ref="${1}" 988 | local rootfs="${2}" 989 | local out_dir="${3}" 990 | local manifest_dir="${4}" 991 | local tarname 992 | local mimetype 993 | local source_info 994 | 995 | if [ -n "${CONTEXT_DIR}" ]; then 996 | _debug "$self: writing to $out_dir and $manifest_dir" 997 | tarname="context.tar" 998 | _tar -C "${CONTEXT_DIR}" \ 999 | --sort=name --mtime=@0 --owner=0 --group=0 --mode='a+rw' --no-xattrs --no-selinux --no-acls \ 1000 | -cf "${out_dir}/${tarname}" . 1001 | mimetype="$(file --brief --mime-type "${out_dir}"/"${tarname}")" 1002 | source_info="${manifest_dir}/${tarname}.json" 1003 | jq \ 1004 | -n \ 1005 | --arg name "${tarname}" \ 1006 | --arg mimetype "${mimetype}" \ 1007 | ' 1008 | { 1009 | "source.artifact.name": $name, 1010 | "source.artifact.mimetype": $mimetype 1011 | } 1012 | ' \ 1013 | > "${source_info}" 1014 | ret=$? 1015 | if [ $ret -ne 0 ] ; then 1016 | return 1 1017 | fi 1018 | fi 1019 | } 1020 | 1021 | # 1022 | # If the caller specified a extra directory 1023 | # 1024 | # slightly special driver, as it has a flag/env passed in, that it uses 1025 | # 1026 | sourcedriver_extra_src_dir() { 1027 | local self="${0#sourcedriver_*}" 1028 | local ref="${1}" 1029 | local rootfs="${2}" 1030 | local out_dir="${3}" 1031 | local manifest_dir="${4}" 1032 | local tarname 1033 | local mimetype 1034 | local source_info 1035 | local counter=0 1036 | 1037 | for extra_src_dir in "${EXTRA_SRC_DIR_ARRAY[@]}" 1038 | do 1039 | _info "adding extra source directory $extra_src_dir" 1040 | _debug "$self: writing to $out_dir and $manifest_dir" 1041 | tarname="extra-src-${counter}.tar" 1042 | ((counter+=1)) 1043 | _tar -C "${extra_src_dir}" \ 1044 | --sort=name --mtime=@0 --owner=0 --group=0 --mode='a+rw' --no-xattrs --no-selinux --no-acls \ 1045 | -cf "${out_dir}/${tarname}" . 1046 | mimetype="$(file --brief --mime-type "${out_dir}"/"${tarname}")" 1047 | source_info="${manifest_dir}/${tarname}.json" 1048 | jq \ 1049 | -n \ 1050 | --arg name "${tarname}" \ 1051 | --arg mimetype "${mimetype}" \ 1052 | ' 1053 | { 1054 | "source.artifact.name": $name, 1055 | "source.artifact.mimetype": $mimetype 1056 | } 1057 | ' \ 1058 | > "${source_info}" 1059 | ret=$? 1060 | if [ $ret -ne 0 ] ; then 1061 | return 1 1062 | fi 1063 | done 1064 | } 1065 | 1066 | 1067 | main() { 1068 | local base_dir 1069 | local input_context_dir 1070 | local input_extra_src_dir_array 1071 | local input_srpm_dir 1072 | local drivers 1073 | local image_ref 1074 | local img_layout 1075 | local list_drivers 1076 | local output_dir 1077 | local push_image_ref 1078 | local ret 1079 | local rootfs 1080 | local src_dir 1081 | local src_img_dir 1082 | local src_img_tag 1083 | local src_name 1084 | local unpack_dir 1085 | local work_dir 1086 | 1087 | declare -a input_extra_src_dir_array 1088 | 1089 | _init "${@}" 1090 | _subcommand "${@}" 1091 | 1092 | base_dir="${BASE_DIR:-$(pwd)/${ABV_NAME}}" 1093 | # using the bash builtin to parse 1094 | while getopts ":hlvDc:s:e:o:b:d:p:" opts; do 1095 | case "${opts}" in 1096 | b) 1097 | base_dir="${OPTARG}" 1098 | ;; 1099 | c) 1100 | input_context_dir=${OPTARG} 1101 | ;; 1102 | e) 1103 | input_extra_src_dir_array+=("${OPTARG}") 1104 | ;; 1105 | d) 1106 | drivers=${OPTARG} 1107 | ;; 1108 | h) 1109 | _usage 1110 | exit 0 1111 | ;; 1112 | l) 1113 | list_drivers=1 1114 | ;; 1115 | o) 1116 | output_dir=${OPTARG} 1117 | ;; 1118 | p) 1119 | push_image_ref=${OPTARG} 1120 | ;; 1121 | s) 1122 | input_srpm_dir=${OPTARG} 1123 | ;; 1124 | v) 1125 | _version 1126 | exit 0 1127 | ;; 1128 | D) 1129 | export DEBUG=1 1130 | ;; 1131 | *) 1132 | _usage 1133 | exit 1 1134 | ;; 1135 | esac 1136 | done 1137 | shift $((OPTIND-1)) 1138 | 1139 | if [ -n "${list_drivers}" ] ; then 1140 | set | grep '^sourcedriver_.* () ' | tr -d ' ()' 1141 | exit 0 1142 | fi 1143 | 1144 | # "local" variables are not set in `env`, but are seen in `set` 1145 | if [ "$(set | grep -c '^input_')" -eq 0 ] ; then 1146 | _error "provide an input (example: $(basename "${0}") -e ./my-sources/ )" 1147 | fi 1148 | 1149 | # These three variables are slightly special, in that they're globals that 1150 | # specific drivers will expect. 1151 | export CONTEXT_DIR="${CONTEXT_DIR:-$input_context_dir}" 1152 | export EXTRA_SRC_DIR_ARRAY=("${input_extra_src_dir_array[@]}") 1153 | export SRPM_DIR="${SRPM_DIR:-$input_srpm_dir}" 1154 | 1155 | output_dir="${OUTPUT_DIR:-$output_dir}" 1156 | 1157 | export TMPDIR="${base_dir}/tmp" 1158 | if [ -d "${TMPDIR}" ] ; then 1159 | _rm_rf "${TMPDIR}" 1160 | fi 1161 | _mkdir_p "${TMPDIR}" 1162 | ret=$? 1163 | if [ ${ret} -ne 0 ] ; then 1164 | _error "failed to mkdir ${TMP}" 1165 | fi 1166 | 1167 | # setup rootfs to be inspected (if any) 1168 | rootfs="" 1169 | image_ref="" 1170 | src_dir="" 1171 | work_dir="${base_dir}/work" 1172 | 1173 | # if we're not fething an image, then this is basically a nop 1174 | rootfs="$(_mktemp_d)" 1175 | image_ref="scratch" 1176 | src_dir="$(_mktemp_d)" 1177 | work_dir="$(_mktemp_d)" 1178 | 1179 | _debug "image layout: ${img_layout}" 1180 | _debug "rootfs dir: ${rootfs}" 1181 | 1182 | # clear prior driver's info about source to insert into Source Image 1183 | _rm_rf "${work_dir}/driver" 1184 | 1185 | if [ -n "${drivers}" ] ; then 1186 | # clean up the args passed by the caller ... 1187 | drivers="$(echo "${drivers}" | tr ',' ' '| tr '\n' ' ')" 1188 | else 1189 | drivers="$(set | grep '^sourcedriver_.* () ' | tr -d ' ()' | tr '\n' ' ')" 1190 | fi 1191 | 1192 | # Prep the OCI layout for the source image 1193 | src_img_dir="$(_mktemp_d)" 1194 | src_img_tag="latest-source" # XXX this tag needs to be a reference to the image built from 1195 | layout_new "${src_img_dir}" "${src_img_tag}" 1196 | 1197 | _info "calling source collection drivers" 1198 | # iterate on the drivers 1199 | #for driver in sourcedriver_rpm_fetch ; do 1200 | for driver in ${drivers} ; do 1201 | _info " --> ${driver#sourcedriver_*}" 1202 | _mkdir_p "${src_dir}/${driver#sourcedriver_*}" 1203 | _mkdir_p "${work_dir}/driver/${driver#sourcedriver_*}" 1204 | $driver \ 1205 | "${image_ref}" \ 1206 | "${rootfs}" \ 1207 | "${src_dir}/${driver#sourcedriver_*}" \ 1208 | "${work_dir}/driver/${driver#sourcedriver_*}" 1209 | ret=$? 1210 | if [ $ret -ne 0 ] ; then 1211 | _error "$driver failed" 1212 | fi 1213 | 1214 | # walk the driver output to determine layers to be added 1215 | find "${work_dir}/driver/${driver#sourcedriver_*}" -type f -name '*.json' | while read -r src_json ; do 1216 | src_name=$(basename "${src_json}" .json) 1217 | layout_insert \ 1218 | "${src_img_dir}" \ 1219 | "${src_dir}/${driver#sourcedriver_*}/${src_name}" \ 1220 | "/${driver#sourcedriver_*}/${src_name}" \ 1221 | "${src_json}" \ 1222 | "${src_img_tag}" 1223 | ret=$? 1224 | if [ $ret -ne 0 ] ; then 1225 | # TODO probably just _error here to exit 1226 | _warn "failed to insert layout layer for ${src_name}" 1227 | fi 1228 | done 1229 | done 1230 | 1231 | _info "packed 'oci:$src_img_dir:${src_img_tag}'" 1232 | 1233 | # TODO maybe look to a directory like /usr/libexec/BuildSourceImage/drivers/ for drop-ins to run 1234 | 1235 | _info "succesfully packed 'oci:${src_img_dir}:${src_img_tag}'" 1236 | _debug "$(skopeo inspect oci:"${src_img_dir}":"${src_img_tag}")" 1237 | 1238 | # dir isn't used anymore but contains all rpms which take space 1239 | _rm_rf "${src_dir}" 1240 | 1241 | ## if an output directory is provided then save a copy to it 1242 | if [ -n "${output_dir}" ] ; then 1243 | _mkdir_p "${output_dir}" 1244 | push_img "oci:$src_img_dir:${src_img_tag}" "oci:$output_dir:${src_img_tag}" 1245 | _info "copied to oci:$output_dir:${src_img_tag}" 1246 | fi 1247 | 1248 | if [ -n "${push_image_ref}" ] ; then 1249 | # XXX may have to parse this reference to ensure it is valid, and that it has a `-source` tag 1250 | push_img "oci:$src_img_dir:${src_img_tag}" "${push_image_ref}" 1251 | fi 1252 | 1253 | } 1254 | 1255 | # only exec main if this is being called (this way we can source and test the functions) 1256 | _is_sourced || main "${@}" 1257 | 1258 | # vim:set shiftwidth=4 softtabstop=4 expandtab: 1259 | --------------------------------------------------------------------------------