├── .chainguard └── source.yaml ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build-samples.yml │ ├── build.yaml │ ├── codeql.yaml │ ├── codeql.yml │ ├── go-tests.yaml │ ├── release.yaml │ └── verify.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .ko.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NEWS.md ├── README.md ├── apk.md ├── config └── task.yaml ├── docs ├── apko_file.md ├── build-process.md ├── busybox.md ├── faq.md └── sbom-composition.md ├── examples ├── abseil-regression.yaml ├── alias-only.yaml ├── alpine-amd64.yaml ├── alpine-base-rootless.yaml ├── alpine-base.yaml ├── alpine-python3.yaml ├── alpine-slim.yaml ├── apko-devenv.yaml ├── include.yaml ├── nginx-rootless.yaml ├── nginx.yaml ├── old-glibc.yaml ├── on_top_of_base │ ├── README.md │ ├── base_image.yaml │ └── build.sh ├── options.yaml ├── overlay.yaml ├── wolfi-base.yaml └── wolfi-go.yaml ├── go.mod ├── go.sum ├── hack ├── ci-tests.sh ├── ci │ ├── 00-build.sh │ └── 01-publish.sh ├── make-devenv.sh ├── run-devenv.sh ├── update-golden.sh └── update-packages.sh ├── header.tmpl ├── internal ├── cli │ ├── build-cpio.go │ ├── build-minirootfs.go │ ├── build.go │ ├── build_test.go │ ├── commands.go │ ├── dot.go │ ├── install-keys.go │ ├── lock.go │ ├── lock_test.go │ ├── options.go │ ├── publish.go │ ├── publish_test.go │ ├── show-config.go │ ├── show-packages.go │ └── testdata │ │ ├── apko.lock.json │ │ ├── apko.pre-0.13.lock.json │ │ ├── apko.yaml │ │ ├── bar │ │ └── bar.txt │ │ ├── base_image.apko.yaml │ │ ├── base_image │ │ ├── blobs │ │ │ └── sha256 │ │ │ │ ├── 2580c204e7b254c8988c5cd1840cd58246afafb2479190b6cb3f448341003a5f │ │ │ │ ├── 2ef91a9967f2e1759ea49a8c01cf6a45dd9f9af71fe09bcf2b86175bc4a71314 │ │ │ │ ├── 583625b6164fff3b017f62b9fcd60cb53fff18a7e89ee538212134a13fc29fb1 │ │ │ │ ├── 5a99438a9ced8193f1d71209d0b558fdc0b184aee5cf258e5f7aa9a6ab0f0671 │ │ │ │ ├── 65eba664f408f8a2b2cf6b190255c8b0e8cb16a85d773089ae184ac35d782464 │ │ │ │ └── bf74ddaf55d32ec9672a0a40efc6cb1bf0a167763c18fc22586c8a301167822f │ │ ├── index.json │ │ ├── metadata │ │ │ ├── aarch64 │ │ │ │ └── APKINDEX │ │ │ └── x86_64 │ │ │ │ └── APKINDEX │ │ ├── oci-layout │ │ └── sboms │ │ │ ├── sbom-aarch64.spdx.json │ │ │ ├── sbom-index.spdx.json │ │ │ └── sbom-x86_64.spdx.json │ │ ├── empty-layering.yaml │ │ ├── foo │ │ └── foo.txt │ │ ├── golden │ │ ├── blobs │ │ │ └── sha256 │ │ │ │ ├── 05722e7f4346cd28e43fd2605ece5517af1ed16e22b24aabba500a51a296ce7c │ │ │ │ ├── 13233c203a5e591839f93061a9153446bc2ac0355710f0e80b2a7a47923e8fcd │ │ │ │ ├── 5dc95810fb12276e75b696b9bce59966c34483797baca0518e3aa6417fe06700 │ │ │ │ ├── 6db738b7b040fcbc8d58cd6a09ac06d3a1ccbd132e29cffe33f7e0499b080d03 │ │ │ │ ├── 7ed5a4e62d3da49c1dbbeec81f803d22714c81117a2229f7234463a0c4a8dd0b │ │ │ │ └── 80121a5654f4a873b122ddb556aba614296b156ac3856e102dbf8a33711e23ab │ │ ├── index.json │ │ ├── oci-layout │ │ └── sboms │ │ │ ├── sbom-aarch64.spdx.json │ │ │ ├── sbom-index.spdx.json │ │ │ └── sbom-x86_64.spdx.json │ │ ├── image_on_top.apko.lock.json │ │ ├── image_on_top.apko.yaml │ │ ├── layering.yaml │ │ ├── melange.rsa │ │ ├── melange.rsa.pub │ │ ├── packages │ │ ├── aarch64 │ │ │ ├── APKINDEX.tar.gz │ │ │ ├── pretend-baselayout-1.0.0-r0.apk │ │ │ └── replayout-1.0.0-r0.apk │ │ ├── melange.rsa.pub │ │ └── x86_64 │ │ │ ├── APKINDEX.tar.gz │ │ │ ├── pretend-baselayout-1.0.0-r0.apk │ │ │ └── replayout-1.0.0-r0.apk │ │ ├── pretend-baselayout.melange.yaml │ │ ├── private_packages │ │ ├── package-x.melange.yaml │ │ ├── package-y.melange.yaml │ │ ├── packages │ │ │ ├── aarch64 │ │ │ │ ├── APKINDEX.json │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ ├── package-x-1.0.0-r0.apk │ │ │ │ └── package-y-1.0.0-r0.apk │ │ │ └── x86_64 │ │ │ │ ├── APKINDEX.json │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ ├── package-x-1.0.0-r0.apk │ │ │ │ └── package-y-1.0.0-r0.apk │ │ ├── private_pkg_key.rsa │ │ └── private_pkg_key.rsa.pub │ │ ├── regenerate_golden_top_image.sh │ │ ├── replayout.melange.yaml │ │ └── top_image │ │ ├── blobs │ │ └── sha256 │ │ │ ├── 4b10507918b5efe4c90ac76f16db810f1716a56f03bed52cdd0343d19ba271f6 │ │ │ ├── 583625b6164fff3b017f62b9fcd60cb53fff18a7e89ee538212134a13fc29fb1 │ │ │ ├── 5c4c61f795999d7bd3ff3f58c301048611bec273fe44160b56b4e08979cda440 │ │ │ ├── 6905923b21195a7d29fe6bf23cca9151f99299d09c1d90eb94cff5dc541ed836 │ │ │ ├── 6deb651bac24febb91142c3235682e65b4d2b6736984ce92f608b9842bf9e88b │ │ │ ├── 711ee9486fdd666205191953b580a158b9e7b00f61226ba901207aa1fcf47fc9 │ │ │ ├── bf74ddaf55d32ec9672a0a40efc6cb1bf0a167763c18fc22586c8a301167822f │ │ │ └── c5b9928e3d484d9c545353ab2a45f024c74b16fd44e0d5b2d7d28dca14a8ff4e │ │ ├── index.json │ │ └── oci-layout ├── gen-jsonschema │ ├── generate.go │ └── main.go ├── ldso-cache │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── ldsocache.go │ ├── ldsocache_test.go │ └── testdata │ │ ├── ld.so.cache │ │ ├── ld.so.conf.d │ │ ├── a.conf │ │ └── b.conf │ │ ├── ld.so.conf.glob │ │ ├── ld.so.conf.simple │ │ └── libroot │ │ ├── etc │ │ ├── ld.so.conf │ │ └── ld.so.conf.d │ │ │ ├── libc.conf │ │ │ ├── sdk-v1.conf │ │ │ └── sdk-v2.conf │ │ ├── lib │ │ └── usr │ │ ├── lib │ │ ├── libfoo.so.1 │ │ └── libnosoname.so │ │ └── local │ │ ├── lib │ │ ├── lib64 │ │ └── .emptydir │ │ ├── sdk-v1 │ │ └── lib │ │ │ ├── libsdk.so │ │ │ ├── libsdk.so.1 │ │ │ └── libsdk.so.1.2.3 │ │ └── sdk-v2 │ │ └── lib │ │ ├── libsdk.so │ │ ├── libsdk.so.1 │ │ └── libsdk.so.1.3.1 ├── tarfs │ └── tarfs.go └── tools.go ├── main.go ├── pkg ├── apk │ ├── README.md │ ├── apk │ │ ├── apkindex.go │ │ ├── apkindex_test.go │ │ ├── arch.go │ │ ├── cache.go │ │ ├── common.go │ │ ├── common_test.go │ │ ├── common_test_nonunix.go │ │ ├── common_test_unix.go │ │ ├── const.go │ │ ├── errors.go │ │ ├── executor.go │ │ ├── implementation.go │ │ ├── implementation_test.go │ │ ├── index.go │ │ ├── install.go │ │ ├── install_test.go │ │ ├── installed.go │ │ ├── installed_test.go │ │ ├── options.go │ │ ├── package.go │ │ ├── package_test.go │ │ ├── releases.go │ │ ├── repo.go │ │ ├── repo_test.go │ │ ├── repository.go │ │ ├── repository_test.go │ │ ├── resolveapk.go │ │ ├── shameful_global_caches.go │ │ ├── testdata │ │ │ ├── APKINDEX.tar.gz │ │ │ ├── README.md │ │ │ ├── alpine-316 │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ ├── alpine-baselayout-3.2.0-r23.apk │ │ │ │ └── alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub │ │ │ ├── alpine-317 │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ ├── alpine-baselayout-3.2.0-r23.apk │ │ │ │ └── alpine-baselayout-3.4.0-r0.apk │ │ │ ├── extracted │ │ │ │ ├── .SIGN.RSA.main@tainer.com-abcdef12.rsa.pub │ │ │ │ ├── APKINDEX │ │ │ │ └── DESCRIPTION │ │ │ ├── hello-0.1.0-r0.apk │ │ │ ├── hello-wolfi-2.12.1-r0.apk │ │ │ ├── replaces │ │ │ │ ├── melange.yaml │ │ │ │ └── replaces-0.0.1-r0.apk │ │ │ ├── root │ │ │ │ └── usr │ │ │ │ │ └── lib │ │ │ │ │ └── apk │ │ │ │ │ └── db │ │ │ │ │ ├── installed │ │ │ │ │ ├── scripts.tar │ │ │ │ │ └── triggers │ │ │ ├── rsa256-signed │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ ├── alpine-baselayout-3.2.0-r23.apk │ │ │ │ ├── rebuild.sh │ │ │ │ └── test-rsa256.rsa.pub │ │ │ └── signing │ │ │ │ ├── APKINDEX.tar.gz │ │ │ │ └── keys │ │ │ │ ├── chainguard-0106f58bac88057c2ff5c2829850df492717a876ed700443550353c7ab23f5a0.rsa.pub │ │ │ │ ├── chainguard-60912bbc46bfc8ed6bda0b50db3a8a5f3c4344d6bc8549ec9b84d96140b475d1.rsa.pub │ │ │ │ └── chainguard-61cb6bccd1f584b007db7be51ebce2b0530a54cc5a94e7650b570d113d537cf3.rsa.pub │ │ ├── transport.go │ │ ├── transport_test.go │ │ ├── util.go │ │ ├── version.go │ │ ├── version_test.go │ │ ├── world.go │ │ └── world_test.go │ ├── auth │ │ ├── auth.go │ │ ├── auth_test.go │ │ └── chainguard.go │ ├── client │ │ └── client.go │ ├── docs │ │ └── CACHE.md │ ├── expandapk │ │ ├── const.go │ │ ├── expandapk.go │ │ ├── split.go │ │ ├── split_test.go │ │ ├── testdata │ │ └── utility.go │ ├── fs │ │ ├── apkfs.go │ │ ├── apkfs_test.go │ │ ├── doc.go │ │ ├── fs.go │ │ ├── memfs.go │ │ ├── memfs_test.go │ │ ├── rwosfs.go │ │ ├── rwosfs_test.go │ │ ├── sub.go │ │ └── testdata │ │ │ └── hello-2.12-r0.apk │ └── signature │ │ └── rsa.go ├── baseimg │ └── base_image.go ├── build │ ├── accounts.go │ ├── accounts_test.go │ ├── apk.go │ ├── build.go │ ├── build_implementation.go │ ├── build_test.go │ ├── busybox.go │ ├── busybox_gen.go │ ├── busybox_gen_versions.go │ ├── busybox_test.go │ ├── busybox_versions.go │ ├── chardevices.go │ ├── installable_from_lock.go │ ├── layers.go │ ├── layers_test.go │ ├── lock.go │ ├── lock_test.go │ ├── multi.go │ ├── oci │ │ ├── consts.go │ │ ├── image.go │ │ ├── image_test.go │ │ ├── index.go │ │ ├── index_test.go │ │ ├── publish.go │ │ └── publish_test.go │ ├── options.go │ ├── paths.go │ ├── sbom.go │ ├── sbom_test.go │ ├── tarball.go │ ├── tarball_test.go │ ├── testdata │ └── types │ │ ├── image_configuration.go │ │ ├── image_configuration_test.go │ │ ├── schema.json │ │ ├── testdata │ │ ├── overlay │ │ │ ├── base.apko.yaml │ │ │ ├── overlay.apko.yaml │ │ │ └── overlay_with_package.apko.yaml │ │ └── users.apko.yaml │ │ ├── types.go │ │ └── types_test.go ├── cpio │ └── layer.go ├── lock │ ├── lock.go │ └── lock_test.go ├── options │ └── options.go ├── passwd │ ├── doc.go │ ├── group.go │ ├── group_test.go │ ├── passwd.go │ ├── passwd_test.go │ └── testdata │ │ ├── group │ │ └── passwd ├── paths │ ├── paths.go │ └── paths_test.go ├── s6 │ ├── s6.go │ └── supervision_tree.go ├── sbom │ ├── generator │ │ ├── generator.go │ │ └── spdx │ │ │ ├── spdx.go │ │ │ ├── spdx_test.go │ │ │ └── testdata │ │ │ ├── apk_sboms │ │ │ ├── _generate.sh │ │ │ ├── font-ubuntu-0.869-r1.spdx.json │ │ │ ├── libattr1-2.5.1-r2.spdx.json │ │ │ ├── logstash-8-8.15.3-r4.spdx.json │ │ │ ├── logstash-8-compat-8.15.3-r4.spdx.json │ │ │ ├── unbound-1.23.0-r0.spdx.json │ │ │ ├── unbound-config-1.23.0-r0.spdx.json │ │ │ └── unbound-libs-1.23.0-r0.spdx.json │ │ │ └── expected_image_sboms │ │ │ ├── custom-license.spdx.json │ │ │ ├── no-supplier.spdx.json │ │ │ ├── package-deduplicating.spdx.json │ │ │ └── unbound-package-dedupe.spdx.json │ ├── options │ │ ├── options.go │ │ └── options_test.go │ └── sbom.go ├── tarfs │ ├── fs.go │ ├── fs_test.go │ └── testdata ├── vcs │ ├── vcs.go │ └── vcs_unit_test.go └── vfs │ ├── dirfs.go │ ├── dirfs_unit_test.go │ ├── fsmode.go │ ├── fsmode_darwin.go │ ├── fsmode_linux.go │ ├── testdata │ └── etc │ │ └── motd │ ├── vfs.go │ └── vfs_unit_test.go └── release.md /.chainguard/source.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | spec: 5 | authorities: 6 | - keyless: 7 | # allow commits signed by users using GitHub or Google OIDC 8 | identities: 9 | - issuer: https://accounts.google.com 10 | - issuer: https://github.com/login/oauth 11 | - key: 12 | # allow commits signed by GitHub, e.g. the UI 13 | kms: https://github.com/web-flow.gpg 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [{*.go,*.go2}] 9 | indent_style = tab 10 | indent_size = 4 11 | 12 | [{*.yaml,*.yml}] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | push: 7 | branches: [ "main" ] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build: 13 | name: build 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | 19 | steps: 20 | - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 21 | with: 22 | egress-policy: audit 23 | 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | 28 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 29 | with: 30 | go-version-file: 'go.mod' 31 | check-latest: true 32 | 33 | - name: build 34 | run: | 35 | make apko 36 | ./apko version 37 | 38 | - name: Setup cosign (needed in CI tests) 39 | uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 40 | 41 | - name: Setup crane (needed in CI tests) 42 | uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4 43 | 44 | - name: Run CI tests in hack/ci/ 45 | run: | 46 | make ci 47 | 48 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 49 | with: 50 | install-only: true 51 | 52 | - name: snapshot 53 | timeout-minutes: 30 54 | run: | 55 | make snapshot 56 | ./dist/apko-build_linux_amd64_v1/apko version 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CODEQL_EXTRACTOR_GO_BUILD_TRACING: true 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | analyze: 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | contents: read 20 | security-events: write 21 | 22 | steps: 23 | - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 24 | with: 25 | egress-policy: audit 26 | 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | persist-credentials: false 30 | 31 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 32 | with: 33 | go-version-file: 'go.mod' 34 | check-latest: true 35 | 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b 38 | with: 39 | languages: go 40 | 41 | - name: build 42 | run: make apko 43 | 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b 46 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '36 8 * * 6' 21 | 22 | permissions: {} 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze (${{ matrix.language }}) 27 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 28 | permissions: 29 | # required for all workflows 30 | security-events: write 31 | 32 | # required to fetch internal or private CodeQL packs 33 | packages: read 34 | 35 | # only required for workflows in private repositories 36 | actions: read 37 | contents: read 38 | 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | include: 43 | - language: go 44 | build-mode: autobuild 45 | 46 | steps: 47 | - name: Harden the runner (Audit all outbound calls) 48 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 49 | with: 50 | egress-policy: audit 51 | 52 | - name: Checkout repository 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 54 | with: 55 | persist-credentials: false 56 | 57 | - name: Initialize CodeQL 58 | uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 59 | with: 60 | languages: ${{ matrix.language }} 61 | build-mode: ${{ matrix.build-mode }} 62 | 63 | - name: Perform CodeQL Analysis 64 | uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 65 | with: 66 | category: "/language:${{matrix.language}}" 67 | -------------------------------------------------------------------------------- /.github/workflows/go-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Go Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | 18 | steps: 19 | - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 20 | with: 21 | egress-policy: audit 22 | 23 | - name: Checkout code 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | 28 | - name: Install Go 29 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 30 | with: 31 | go-version-file: 'go.mod' 32 | check-latest: true 33 | 34 | - name: Test 35 | run: make test 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Create Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | cli: 13 | # Only release CLI for tagged releases 14 | if: startsWith(github.event.ref, 'refs/tags/v') 15 | 16 | name: Release the CLI 17 | runs-on: ubuntu-latest 18 | 19 | # https://docs.github.com/en/actions/reference/authentication-in-a-workflow 20 | permissions: 21 | id-token: write 22 | contents: write 23 | 24 | steps: 25 | - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 26 | with: 27 | egress-policy: audit 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 29 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 30 | with: 31 | go-version-file: 'go.mod' 32 | check-latest: true 33 | 34 | - uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 35 | 36 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 37 | with: 38 | install-only: true 39 | 40 | - name: Release 41 | run: make release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/verify.yaml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | golangci: 13 | name: lint 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | 19 | steps: 20 | - name: Harden the runner (Audit all outbound calls) 21 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 22 | with: 23 | egress-policy: audit 24 | 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | persist-credentials: false 28 | 29 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 30 | with: 31 | go-version-file: 'go.mod' 32 | check-latest: true 33 | 34 | - name: golangci-lint 35 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 36 | with: 37 | version: v1.63 38 | 39 | - run: | 40 | make generate 41 | go mod tidy 42 | git diff --exit-code 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | /apko 8 | dist/ 9 | bin/ 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | .vscode 20 | .idea 21 | tmp/ 22 | _output 23 | 24 | # SBOM outputs from apko 25 | sbom-* 26 | 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | forbidigo: 3 | forbid: 4 | - ^print.*$ 5 | goheader: 6 | template-path: header.tmpl 7 | values: 8 | regexp: 9 | ws: "\\s*" 10 | goimports: 11 | local-prefixes: chainguard.dev/apko 12 | gosec: 13 | excludes: 14 | # TODO: review those and remove if possible 15 | - G115 # integer overflow conversion uint64 -> int 16 | 17 | linters: 18 | enable: 19 | - asciicheck 20 | - errcheck 21 | - errorlint 22 | - forbidigo 23 | - gofmt 24 | - goimports 25 | - gosec 26 | - gocritic 27 | - importas 28 | - prealloc 29 | - misspell 30 | - stylecheck 31 | - tparallel 32 | - unconvert 33 | - unparam 34 | - unused 35 | - whitespace 36 | issues: 37 | exclude-rules: 38 | - path: _test\.go 39 | linters: 40 | - errcheck 41 | - gosec 42 | max-issues-per-linter: 0 43 | max-same-issues: 0 44 | run: 45 | issues-exit-code: 1 46 | timeout: 10m 47 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: apko 2 | version: 2 3 | 4 | before: 5 | hooks: 6 | - go mod download 7 | 8 | env: 9 | - COSIGN_YES=true 10 | 11 | builds: 12 | - id: apko-build 13 | binary: apko 14 | main: ./ 15 | env: 16 | - CGO_ENABLED=0 17 | goos: 18 | - darwin 19 | - linux 20 | goarch: 21 | - "386" 22 | - amd64 23 | - arm64 24 | ignore: 25 | - goos: darwin 26 | goarch: "386" 27 | flags: 28 | - -trimpath 29 | mod_timestamp: '{{ .CommitTimestamp }}' 30 | ldflags: 31 | - -extldflags "-static" 32 | - "{{ .Env.LDFLAGS }}" 33 | 34 | signs: 35 | - id: apko-cosign 36 | cmd: cosign 37 | certificate: "${artifact}.crt" 38 | args: ["sign-blob", "--output-signature", "${signature}", "--output-certificate", "${certificate}", "${artifact}", "--yes"] 39 | artifacts: all 40 | 41 | archives: 42 | - files: 43 | - LICENSE 44 | wrap_in_directory: true 45 | 46 | checksum: 47 | name_template: 'checksums.txt' 48 | 49 | snapshot: 50 | version_template: "{{ .Tag }}-next" 51 | 52 | changelog: 53 | sort: asc 54 | filters: 55 | exclude: 56 | - '^docs:' 57 | - '^test:' 58 | 59 | release: 60 | draft: false 61 | prerelease: false 62 | name_template: "Release {{ .Tag }}" 63 | -------------------------------------------------------------------------------- /.ko.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Chainguard, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | baseImageOverrides: 16 | chainguard.dev/apko: cgr.dev/chainguard/wolfi-base 17 | 18 | builds: 19 | - id: apko 20 | env: 21 | - CGO_ENABLED=0 22 | flags: 23 | - -trimpath 24 | ldflags: 25 | - -extldflags "-static" 26 | - "{{ .Env.LDFLAGS }}" 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Thank you for you interest in apko! You are welcome here. 4 | 5 | apko and [melange](https://github.com/chainguard-dev/melange) are open 6 | source projects, always open for outside contributors. 7 | 8 | If you are looking for a place to start take a look at the 9 | [open issues](https://github.com/chainguard-dev/apko/issues), 10 | especially those marked with 11 | [good-first-issue](https://github.com/chainguard-dev/apko/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). 12 | 13 | ## Setting Up a Development Environment 14 | 15 | Running apko requires an environment with apk available. To make development 16 | easier under any platform, we have created a script that uses apko itself to 17 | bootstrap a development environment under any platform with docker installed. 18 | 19 | To run it, simply execute the following script from the top of the apko 20 | repository: 21 | 22 | ```bash 23 | ./hack/make-devenv.sh 24 | ``` 25 | 26 | That command should use apko to create a development image with some 27 | useful tools. It will load the devel image into your local docker daemon and 28 | drop you into a shell. The apko repository in your local machine will be 29 | mounted in the current working directory. 30 | 31 | You can use your editor to change the apko code and execute apko in the 32 | development shell to test it out: 33 | 34 | ``` 35 | go run ./main.go 36 | 37 | e5cf7d79f68b:/apko# go run ./main.go version 38 | _ ____ _ __ ___ 39 | / \ | _ \ | |/ / / _ \ 40 | / _ \ | |_) | | ' / | | | | 41 | / ___ \ | __/ | . \ | |_| | 42 | /_/ \_\ |_| |_|\_\ \___/ 43 | apko 44 | 45 | GitVersion: 46 | GitCommit: unknown 47 | GitTreeState: unknown 48 | BuildDate: unknown 49 | GoVersion: go1.18 50 | Compiler: gc 51 | Platform: linux/amd64 52 | 53 | 54 | ``` 55 | 56 | When done developing, simply exit the development shell. We would love to hear 57 | about your experience in the development shell and any ideas you may have! 58 | 59 | ## Linting and Tests 60 | 61 | Before submitting a pull request, make sure tests and lints do not complain. 62 | Make sure you have go 1.18 and 63 | [golangci-lint](https://golangci-lint.run/usage/install/) installed and try 64 | running the linter and tests: 65 | 66 | ``` 67 | go test ./... 68 | golangci-lint run 69 | ``` 70 | -------------------------------------------------------------------------------- /apk.md: -------------------------------------------------------------------------------- 1 | # apk implementation 2 | 3 | `apko` relies on parsing apk packages and installing them in the appropriate directories. 4 | 5 | The implementation is in the Chainguard-provided OSS native go implementation of APK 6 | [go-apk](https://pkg.go.dev/chainguard.dev/apko/pkg/apk). 7 | 8 | Please see the documentation for that package. 9 | -------------------------------------------------------------------------------- /config/task.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | apiVersion: tekton.dev/v1beta1 16 | kind: Task 17 | metadata: 18 | name: apko 19 | spec: 20 | description: "An apko task for building images." 21 | params: 22 | - name: dev.mink.sources.bundle 23 | description: A self-extracting container image of source 24 | - name: dev.mink.images.target 25 | description: Where to publish an image. 26 | - name: path 27 | description: The bundle-relative path to the config file to build. 28 | default: .apko.yaml 29 | 30 | results: 31 | - name: IMAGES 32 | description: The list of images produced by this task (for chains) 33 | - name: dev.mink.images.digest 34 | description: The digest of the resulting image. 35 | 36 | steps: 37 | - name: extract-bundle 38 | image: $(params["dev.mink.sources.bundle"]) 39 | workingDir: /workspace 40 | 41 | - name: build-and-push-image 42 | image: ko://chainguard.dev/apko 43 | workingDir: /workspace 44 | args: 45 | - publish 46 | - --image-refs=/tekton/results/IMAGES 47 | - $(params["path"]) 48 | - $(params["dev.mink.images.target"]) 49 | 50 | # mink wants just the digest in its result, but the above produces 51 | # a fully qualified digest. 52 | - name: massage-digest 53 | image: gcr.io/distroless/base:debug-nonroot 54 | command: ["/busybox/sh", "-c"] 55 | args: 56 | - echo -n $(cat /tekton/results/IMAGES | cut -d'@' -f 2) > /tekton/results/dev.mink.images.digest 57 | -------------------------------------------------------------------------------- /docs/busybox.md: -------------------------------------------------------------------------------- 1 | # Busybox 2 | 3 | Many images depend on [busybox](https://busybox.net). 4 | 5 | `apko` supports busybox. However, rather than running `busybox --install` to generate all of the symlinks, 6 | which would require a runner environment and working out the paths, `apko` installs them directly. 7 | 8 | `apko` keeps an in-program list of symlinks for each supported version of busybox. At the end of the 9 | installation process, if it finds `/bin/busybox`, it then looks in the apk installed database at 10 | `/usr/lib/apk/db/installed` and determines which version of busybox is installed. It simplifies that 11 | version to the basic semver, e.g. v1.36.1-r3 becomes 1.36.1. It then finds that version in its 12 | own list of symlinks. 13 | 14 | If it cannot find that version, it will fall back to the latest version it has in its list. 15 | 16 | ## Supported Versions 17 | 18 | `apko` tries to support all semver versions, including major, minor and patch, but not release candidates or 19 | other such subversions, from 1.32.1 up. This is set in [busybox_gen.go](../pkg/build/busybox_gen.go). 20 | 21 | ## Generating Symlinks 22 | 23 | To regenerate the symlinks, run the following from the repository root: 24 | 25 | ```sh 26 | go generate -tags busybox_versions ./pkg/build/busybox_gen.go 27 | ``` 28 | 29 | This will generate `pkg/build/busybox_versions.go`, which will be compiled into the program. 30 | 31 | You do **not** need to rerun the generation with each compile; only if you want to update the list of versions 32 | and their symlinks. 33 | 34 | It is a fairly expensive process, downloading the entire busybox git repository, and then processing it. 35 | 36 | To prevent that happening with each `go generate`, it is restricted to the tags shown above. 37 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | Some questions that have come up about `apko`. 4 | 5 | ## Is it a valid use case to build on top of apko generated images with other tooling e.g. Dockerfiles? 6 | 7 | Yes! 8 | 9 | We encourage people to use `apko` and `melange` to build their own images, but in some cases 10 | existing or alternative tooling will be more appropriate. 11 | 12 | If you want to add files or packages outside of APK artifacts on top of an `apko` generated base 13 | image, then using Dockerfiles or similar is definitely a supported use case. `apko` aims to 14 | enable people to use the patterns that suit them and their workflow best. 15 | 16 | Having said that, we believe using APK to install everything offers big advantages with regards 17 | to compliance and auditability - there will be SBOMs for all the contents and security scanners 18 | will be aware of all installed software. 19 | 20 | ## Can we use `apko` as a library? 21 | 22 | Making an `apko` library isn't a current priority, but we would welcome patches towards this 23 | goal. 24 | 25 | If you want to wrap the CLI, note that breaking changes are possible, but will be announced in 26 | `NEWS.md`. 27 | -------------------------------------------------------------------------------- /examples/abseil-regression.yaml: -------------------------------------------------------------------------------- 1 | # This was very slow once. 2 | contents: 3 | keyring: 4 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 5 | repositories: 6 | - https://packages.wolfi.dev/os 7 | 8 | packages: 9 | - abseil-cpp-dev 10 | - pkgconf 11 | 12 | archs: 13 | - arm64 14 | -------------------------------------------------------------------------------- /examples/alias-only.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - openssh-client 6 | 7 | archs: 8 | - x86_64 9 | -------------------------------------------------------------------------------- /examples/alpine-amd64.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-base 6 | 7 | cmd: /bin/sh -l 8 | 9 | archs: 10 | - amd64 11 | -------------------------------------------------------------------------------- /examples/alpine-base-rootless.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-base 6 | 7 | accounts: 8 | groups: 9 | - groupname: nonroot 10 | gid: 10000 11 | users: 12 | - username: nonroot 13 | uid: 10000 14 | run-as: nonroot 15 | 16 | cmd: /bin/sh -l 17 | 18 | # optional environment configuration 19 | environment: 20 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 21 | 22 | archs: 23 | - amd64 24 | -------------------------------------------------------------------------------- /examples/alpine-base.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-base 6 | 7 | cmd: /bin/sh -l 8 | 9 | # optional environment configuration 10 | environment: 11 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 12 | 13 | archs: 14 | - amd64 15 | -------------------------------------------------------------------------------- /examples/alpine-python3.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-base 6 | - python3 7 | 8 | cmd: /bin/sh -l 9 | 10 | # optional environment configuration 11 | environment: 12 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 13 | 14 | archs: 15 | - amd64 16 | -------------------------------------------------------------------------------- /examples/alpine-slim.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-baselayout-data 6 | - apk-tools 7 | - busybox 8 | 9 | # optional environment configuration 10 | environment: 11 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 12 | 13 | cmd: /bin/sh -l 14 | 15 | archs: 16 | - amd64 17 | -------------------------------------------------------------------------------- /examples/apko-devenv.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # APKO DEVELOPMENT ENVIRONMENT 16 | # 17 | # This image configuration is used by the apko development environment 18 | # shell script. It preconfigures some tools to make them available in 19 | # the dev shell. 20 | # 21 | contents: 22 | repositories: 23 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 24 | - https://dl-cdn.alpinelinux.org/alpine/edge/community 25 | packages: 26 | - alpine-base 27 | - go 28 | - git 29 | - jq 30 | - tree 31 | - make 32 | - docker-cli 33 | entrypoint: 34 | command: /bin/sh -l 35 | 36 | archs: 37 | - amd64 38 | -------------------------------------------------------------------------------- /examples/include.yaml: -------------------------------------------------------------------------------- 1 | include: examples/alpine-base.yaml 2 | -------------------------------------------------------------------------------- /examples/nginx-rootless.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 4 | packages: 5 | - alpine-baselayout 6 | - nginx 7 | 8 | entrypoint: 9 | type: service-bundle 10 | services: 11 | nginx: /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;" 12 | 13 | accounts: 14 | groups: 15 | - groupname: nginx 16 | gid: 10000 17 | users: 18 | - username: nginx 19 | uid: 10000 20 | run-as: nginx 21 | 22 | # optional environment configuration 23 | environment: 24 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 25 | 26 | # optional path mutations 27 | paths: 28 | - path: /run/nginx 29 | type: directory 30 | uid: 10000 31 | gid: 10000 32 | permissions: 0o755 33 | - path: /etc/nginx/http.d/default.conf 34 | type: hardlink 35 | source: /usr/share/nginx/http-default_server.conf 36 | uid: 10000 37 | gid: 10000 38 | permissions: 0o644 39 | 40 | archs: 41 | - amd64 42 | -------------------------------------------------------------------------------- /examples/nginx.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 4 | repositories: 5 | - https://packages.wolfi.dev/os 6 | packages: 7 | - wolfi-baselayout 8 | - nginx 9 | 10 | entrypoint: 11 | type: service-bundle 12 | services: 13 | nginx: /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;" 14 | 15 | # optional environment configuration 16 | environment: 17 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin 18 | 19 | # optional user mutations 20 | accounts: 21 | groups: 22 | - groupname: nginx 23 | gid: 10000 24 | users: 25 | - username: nginx 26 | uid: 10000 27 | 28 | # optional path mutations 29 | paths: 30 | - path: /run/nginx 31 | type: directory 32 | uid: 10000 33 | gid: 10000 34 | permissions: 0o755 35 | - path: /etc/nginx/http.d/default.conf 36 | type: hardlink 37 | source: /etc/nginx/nginx.conf.default 38 | uid: 10000 39 | gid: 10000 40 | permissions: 0o644 41 | 42 | work-dir: /usr/share/nginx 43 | 44 | annotations: 45 | foo: bar 46 | 47 | archs: 48 | - amd64 49 | -------------------------------------------------------------------------------- /examples/old-glibc.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - https://packages.wolfi.dev/os 4 | keyring: 5 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 6 | packages: 7 | # Testing that old packages with potentially invalid SBOMs can produce a valid image SBOM. 8 | - glibc=2.36-r3 9 | - binutils=2.39-r4 10 | - git=2.39.0-r0 11 | - openssl=3.0.7-r0 12 | - sysstat=12.6.2-r0 13 | - libcrypto3=3.0.8-r0 14 | 15 | archs: 16 | - x86_64 17 | - aarch64 18 | -------------------------------------------------------------------------------- /examples/on_top_of_base/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This example demnostrates the possibility to build with APKO on top of base image. 4 | 5 | **WARNING** Still work in progress, subject to breaking API change. 6 | 7 | **Current limitations** 8 | 9 | * Base image must be present on the filesystem 10 | * Build requires a lockfile 11 | * The config may only add packages. Other top level elements are banned. 12 | * SBOM generation is incomplete - omits any contents of the base image. 13 | 14 | ## How to use 15 | 16 | Currently the functionality requires the base image and it's metadata to be present on the filesystem. 17 | 18 | For the `apko.yaml` example, do the following steps: 19 | 20 | 1. Go the apko repository root directory 21 | 2. Run 22 | 23 | ``` 24 | ./examples/on_top_of_base/build.sh 25 | ``` 26 | 27 | It optionally accepts the apko binary as first argument. By default it will use apko from PATH. -------------------------------------------------------------------------------- /examples/on_top_of_base/base_image.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | baseimage: 3 | image: ./examples/on_top_of_base/base_image 4 | apkindex: ./examples/on_top_of_base/apkindexes 5 | repositories: 6 | - https://packages.wolfi.dev/os 7 | keyring: 8 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 9 | packages: 10 | # Already in base, should not be added again. 11 | - glibc 12 | # Should be added with all dependencies 13 | - clang 14 | 15 | archs: 16 | - x86_64 -------------------------------------------------------------------------------- /examples/on_top_of_base/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script for building image on top of base with apko. Must be run from the root of github repository. 4 | 5 | apko_binary="${1:-apko}" 6 | 7 | EXAMPLE_DIR=./examples/on_top_of_base 8 | BASE_IMAGE=cgr.dev/chainguard/wolfi-base:latest 9 | BASE_IMAGE_DIR="$EXAMPLE_DIR/base_image" 10 | APKINDEX_DIR="$EXAMPLE_DIR/apkindexes" 11 | FS_DUMP_DIR="$EXAMPLE_DIR/fs_dump" 12 | 13 | # Pull base image 14 | crane pull "$BASE_IMAGE" "$BASE_IMAGE_DIR" --format=oci 15 | # Prepare apkindex for base image 16 | mkdir -p "$FS_DUMP_DIR" 17 | crane export "$BASE_IMAGE" "$FS_DUMP_DIR/fs.tar" 18 | tar -C "$FS_DUMP_DIR" -xf "$FS_DUMP_DIR/fs.tar" 19 | mkdir -p "$APKINDEX_DIR/x86_64/" 20 | cp "$FS_DUMP_DIR/lib/apk/db/installed" "$APKINDEX_DIR/x86_64/APKINDEX" 21 | 22 | "$apko_binary" lock "$EXAMPLE_DIR/base_image.yaml" 23 | 24 | mkdir -p "$EXAMPLE_DIR/top_image" 25 | 26 | "$apko_binary" build "$EXAMPLE_DIR/base_image.yaml" base_image:latest "$EXAMPLE_DIR/top_image/" --lockfile="$EXAMPLE_DIR/base_image.lock.json" --sbom=False 27 | -------------------------------------------------------------------------------- /examples/options.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 4 | repositories: 5 | - https://packages.wolfi.dev/os 6 | packages: 7 | - ca-certificates-bundle 8 | - wolfi-baselayout 9 | - glibc 10 | - libgcc 11 | 12 | archs: 13 | - x86_64 14 | - aarch64 15 | -------------------------------------------------------------------------------- /examples/overlay.yaml: -------------------------------------------------------------------------------- 1 | include: examples/alpine-base.yaml 2 | 3 | contents: 4 | packages: 5 | - nano 6 | -------------------------------------------------------------------------------- /examples/wolfi-base.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 4 | repositories: 5 | - https://packages.wolfi.dev/os 6 | packages: 7 | - wolfi-base 8 | 9 | cmd: /bin/sh -l 10 | 11 | archs: 12 | - x86_64 13 | - aarch64 14 | -------------------------------------------------------------------------------- /examples/wolfi-go.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub 4 | repositories: 5 | - https://packages.wolfi.dev/os 6 | 7 | packages: 8 | - wolfi-base 9 | - go 10 | 11 | environment: 12 | PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin:/root/go/bin 13 | 14 | archs: 15 | - x86_64 16 | - arm64 17 | -------------------------------------------------------------------------------- /hack/ci-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -ex 7 | 8 | # Go to repo root 9 | cd "$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )/../" 10 | 11 | if [[ ! -f apko ]]; then 12 | echo "Please first run \"make apko\". Exiting." 13 | exit 1 14 | fi 15 | 16 | export APKO=$(pwd)/apko 17 | for exe in `find hack/ci -name '*.sh' | sort`; do 18 | echo "Running test: ${exe}" 19 | "${exe}" 20 | done 21 | -------------------------------------------------------------------------------- /hack/ci/00-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -ex 7 | 8 | OUTPUT_TAR="output.tar" 9 | REF="apko.local/ci-testing:test" 10 | 11 | trap "rm -f ${OUTPUT_TAR}" EXIT 12 | 13 | for f in examples/wolfi-base.yaml; do 14 | echo "=== building $f" 15 | 16 | REF="apko.local/ci-testing:$(basename ${f})" 17 | "${APKO}" build "${f}" "${REF}" "${OUTPUT_TAR}" --arch amd64,arm64 18 | 19 | # Subtest #1: Does it load? 20 | ARCH_REF="$(docker load < output.tar | grep "Loaded image" | sed 's/^Loaded image: //' | head -1)" 21 | 22 | # Subtest #2: Can we run it? 23 | docker run --rm "${ARCH_REF}" echo hello | grep hello 24 | done 25 | -------------------------------------------------------------------------------- /hack/ci/01-publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -ex 7 | 8 | REGISTRY_BASE_IMAGE="index.docker.io/library/registry:2.8.1" 9 | REGISTRY_CONTAINER_NAME="ci-testing-registry" 10 | PORT=${PORT:-${RANDOM}} 11 | 12 | trap "rm -f *.sbom.json && \ 13 | docker rm -f ${REGISTRY_CONTAINER_NAME}" EXIT 14 | 15 | docker rm -f "${REGISTRY_CONTAINER_NAME}" 16 | docker run --name "${REGISTRY_CONTAINER_NAME}" \ 17 | -d -p ${PORT}:5000 "${REGISTRY_BASE_IMAGE}" 18 | 19 | for f in examples/wolfi-base.yaml; do 20 | echo "=== building $f" 21 | 22 | REF="localhost:${PORT}/ci-testing:$(basename ${f})" 23 | img=$("${APKO}" publish "${f}" "${REF}" --arch amd64,arm64) 24 | 25 | # Run the image. 26 | docker run --rm ${img} echo hello | grep hello 27 | 28 | # Each platform should contain platform-specific etc/apk/arch file. 29 | crane export --platform linux/amd64 "${REF}" | tar -Ox etc/apk/arch | grep x86_64 30 | crane export --platform linux/arm64 "${REF}" | tar -Ox etc/apk/arch | grep aarch64 31 | done 32 | -------------------------------------------------------------------------------- /hack/make-devenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | BUILDER_ALPINE_TAG="edge@sha256:3e44438281baf26907675b99c9a4a421c4d4a57c954120327e703aa8329086bd" 7 | DEVENV_IMAGE_TARBALL="apko-inception.tar.gz" 8 | IMAGE_TAG="apko-inception" 9 | ARCH=$(uname -m) 10 | 11 | # Map apk-style arch string to the OCI-style equivalent 12 | case "$ARCH" in 13 | "x86") 14 | ARCH="_386";; 15 | "x86_64") 16 | ARCH="amd64";; 17 | "aarch64") 18 | ARCH="arm64";; 19 | "armhf") 20 | ARCH="armv6";; 21 | esac 22 | 23 | checkrepo() { 24 | grep "module chainguard.dev/apko" go.mod >/dev/null 2>&1 && return 25 | echo; 26 | echo "Please run me from the apko repository root. Thank you!"; 27 | echo 28 | exit 1; 29 | } 30 | 31 | run_builder() { 32 | if ! (docker inspect apko-inception:latest-$ARCH >/dev/null 2>&1 ); then 33 | set -e 34 | mkdir _output > /dev/null 2>&1 || : 35 | docker run --rm -v $(pwd):/apko -w /apko -ti \ 36 | -e BUILD_UID=$(id -u) -e BUILD_GID=$(id -g) \ 37 | alpine:${BUILDER_ALPINE_TAG} \ 38 | /bin/sh hack/make-devenv.sh build_image 39 | load_image 40 | rm _output/${DEVENV_IMAGE_TARBALL} 41 | fi 42 | run 43 | } 44 | 45 | build_image() { 46 | set -e 47 | cat /etc/os-release 48 | apk add go 49 | go run main.go build ./examples/apko-devenv.yaml ${IMAGE_TAG}:latest ./_output/${DEVENV_IMAGE_TARBALL} --sbom=false --arch=$ARCH 50 | chown ${BUILD_UID}:${BUILD_GID} _output/${DEVENV_IMAGE_TARBALL} 51 | } 52 | 53 | load_image() { 54 | set -e 55 | docker rmi ${IMAGE_TAG}:latest-$ARCH 2>&1 || : 56 | docker load < _output/${DEVENV_IMAGE_TARBALL} 57 | } 58 | 59 | run() { 60 | docker run --rm -w /apko -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/apko -ti ${IMAGE_TAG}:latest-$ARCH hack/make-devenv.sh setup 61 | } 62 | 63 | setup() { 64 | echo 65 | echo "Welcome to the apko development environment!" 66 | echo 67 | echo 68 | alias ll="ls -l" 69 | export PS1="[apko] ❯ " 70 | sh -i 71 | } 72 | 73 | checkrepo 74 | case "$1" in 75 | "") 76 | run_builder;; 77 | "build_image") 78 | build_image;; 79 | "run") 80 | run;; 81 | "setup") 82 | setup;; 83 | esac 84 | -------------------------------------------------------------------------------- /hack/run-devenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | function run() { 7 | 8 | if [[ ! -f "./apko" ]]; then 9 | echo "Building apko" 10 | make apko 11 | fi 12 | 13 | ./apko "$@" 14 | } 15 | 16 | function docker_run() { 17 | docker run --rm -w /apko -v $(pwd):/apko --entrypoint /apko/hack/run-devenv.sh apko-inception:latest run $@ 18 | } 19 | 20 | case "$1" in 21 | "run") 22 | run ${@:4};; 23 | *) 24 | docker_run $@;; 25 | esac 26 | 27 | -------------------------------------------------------------------------------- /hack/update-golden.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2023 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -ex 7 | 8 | (cd internal/cli && \ 9 | rm -rf ./testdata/golden && \ 10 | mkdir -p ./testdata/golden/sboms && \ 11 | apko build --sbom-formats spdx --sbom-path ./testdata/golden/sboms ./testdata/apko.yaml golden:latest ./testdata/golden) 12 | -------------------------------------------------------------------------------- /hack/update-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2023 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | set -ex 7 | 8 | (cd internal/cli/testdata && \ 9 | melange build --arch arm64 --arch amd64 -r https://packages.wolfi.dev/os -k https://packages.wolfi.dev/os/wolfi-signing.rsa.pub --signing-key ./melange.rsa pretend-baselayout.melange.yaml && \ 10 | melange build --arch arm64 --arch amd64 -r https://packages.wolfi.dev/os -k https://packages.wolfi.dev/os/wolfi-signing.rsa.pub --signing-key ./melange.rsa replayout.melange.yaml) 11 | (cd internal/cli && 12 | apko lock ./testdata/apko.yaml) 13 | -------------------------------------------------------------------------------- /header.tmpl: -------------------------------------------------------------------------------- 1 | Copyright {{YEAR}} Chainguard, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | {{ ws }}http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /internal/cli/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cli 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | "net/http" 21 | "os" 22 | 23 | "github.com/chainguard-dev/clog/slag" 24 | charmlog "github.com/charmbracelet/log" 25 | cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" 26 | "github.com/spf13/cobra" 27 | "sigs.k8s.io/release-utils/version" 28 | ) 29 | 30 | func New() *cobra.Command { 31 | var workDir string 32 | cwd, err := os.Getwd() 33 | if err != nil { 34 | cwd = "" 35 | } 36 | level := slag.Level(slog.LevelInfo) 37 | cmd := &cobra.Command{ 38 | Use: "apko", 39 | DisableAutoGenTag: true, 40 | SilenceUsage: true, 41 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 42 | http.DefaultTransport = userAgentTransport{http.DefaultTransport} 43 | if workDir != "" { 44 | if err := os.Chdir(workDir); err != nil { 45 | return fmt.Errorf("failed to change dir to %s: %w", workDir, err) 46 | } 47 | } 48 | slog.SetDefault(slog.New(charmlog.NewWithOptions(os.Stderr, charmlog.Options{ReportTimestamp: true, Level: charmlog.Level(level)}))) 49 | return nil 50 | }, 51 | } 52 | cmd.PersistentFlags().Var(&level, "log-level", "log level (e.g. debug, info, warn, error, fatal, panic)") 53 | 54 | cmd.AddCommand(cranecmd.NewCmdAuthLogin("apko")) // apko login 55 | cmd.AddCommand(buildCmd()) 56 | cmd.AddCommand(buildMinirootFS()) 57 | cmd.AddCommand(buildCPIO()) 58 | cmd.AddCommand(showConfig()) 59 | cmd.AddCommand(publish()) 60 | cmd.AddCommand(showPackages()) 61 | cmd.AddCommand(dotcmd()) 62 | cmd.AddCommand(lock()) 63 | cmd.AddCommand(resolve()) 64 | cmd.AddCommand(installKeys()) 65 | cmd.AddCommand(version.Version()) 66 | 67 | cmd.PersistentFlags().StringVarP(&workDir, "workdir", "C", cwd, "working dir (default is current dir where executed)") 68 | return cmd 69 | } 70 | 71 | type userAgentTransport struct{ t http.RoundTripper } 72 | 73 | func (u userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) { 74 | req.Header.Set("User-Agent", fmt.Sprintf("apko/%s", version.GetVersionInfo().GitVersion)) 75 | return u.t.RoundTrip(req) 76 | } 77 | -------------------------------------------------------------------------------- /internal/cli/install-keys.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/chainguard-dev/clog" 9 | "github.com/spf13/cobra" 10 | 11 | "chainguard.dev/apko/pkg/apk/apk" 12 | ) 13 | 14 | func installKeys() *cobra.Command { 15 | return &cobra.Command{ 16 | Use: "install-keys", 17 | Example: `apko install-keys`, 18 | Short: "Discover and install keys for all repositories", 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | ctx := cmd.Context() 22 | log := clog.FromContext(ctx) 23 | 24 | a, err := apk.New() 25 | if err != nil { 26 | return err 27 | } 28 | repos, err := a.GetRepositories() 29 | if err != nil { 30 | return err 31 | } 32 | for _, repo := range repos { 33 | keys, err := a.DiscoverKeys(ctx, repo) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if err := os.MkdirAll("/etc/apk/keys", 0755); err != nil { 39 | return err 40 | } 41 | for _, key := range keys { 42 | fn := filepath.Join("/etc/apk/keys", key.ID) 43 | if err := os.WriteFile(fn, key.Bytes, 0o644); err != nil { //nolint: gosec 44 | return fmt.Errorf("failed to write key %s: %w", key.ID, err) 45 | } 46 | log.With("repo", repo).Infof("wrote %s", fn) 47 | } 48 | } 49 | return nil 50 | }, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/cli/options.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type publishOpt struct { 4 | local bool 5 | tags []string 6 | } 7 | 8 | // PublishOption is an option for publishing 9 | type PublishOption func(*publishOpt) error 10 | 11 | // WithLocal sets whether to publish image to local Docker daemon. 12 | func WithLocal(local bool) PublishOption { 13 | return func(p *publishOpt) error { 14 | p.local = local 15 | return nil 16 | } 17 | } 18 | 19 | // WithTags tags to use 20 | func WithTags(tags ...string) PublishOption { 21 | return func(p *publishOpt) error { 22 | p.tags = tags 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/cli/testdata/apko.pre-0.13.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1", 3 | "contents": { 4 | "keyring": [ 5 | { 6 | "name": "./testdata/melange.rsa.pub", 7 | "url": "./testdata/melange.rsa.pub" 8 | } 9 | ], 10 | "repositories": [ 11 | { 12 | "name": "./testdata/packages/x86_64", 13 | "url": "./testdata/packages/x86_64/APKINDEX.tar.gz", 14 | "architecture": "x86_64" 15 | }, 16 | { 17 | "name": "./testdata/packages/aarch64", 18 | "url": "./testdata/packages/aarch64/APKINDEX.tar.gz", 19 | "architecture": "aarch64" 20 | } 21 | ], 22 | "packages": [ 23 | { 24 | "name": "pretend-baselayout", 25 | "url": "./testdata/packages/x86_64/pretend-baselayout-1.0.0-r0.apk", 26 | "version": "1.0.0-r0", 27 | "architecture": "x86_64", 28 | "signature": { 29 | "range": "bytes=0-684", 30 | "checksum": "sha1-e4mtQNHHROPVjqH3vNZUtKTu5Uc=" 31 | }, 32 | "control": { 33 | "range": "bytes=685-964", 34 | "checksum": "sha1-cs+Hlyu5sY+1mmwKPedcRWj8E24=" 35 | }, 36 | "data": { 37 | "range": "bytes=965-2899", 38 | "checksum": "sha256-6Gbikm5tHIqetbTVz/wPcmtCcrO17ed1vPM6xuR0IrU=" 39 | } 40 | }, 41 | { 42 | "name": "replayout", 43 | "url": "./testdata/packages/x86_64/replayout-1.0.0-r0.apk", 44 | "version": "1.0.0-r0", 45 | "architecture": "x86_64", 46 | "signature": { 47 | "range": "bytes=0-684", 48 | "checksum": "sha1-UCaIAliqYaIrol8/H5PAi8J5ihg=" 49 | }, 50 | "control": { 51 | "range": "bytes=685-974", 52 | "checksum": "sha1-ADqt8AXOdbDPVa9UeGNJngcURrk=" 53 | }, 54 | "data": { 55 | "range": "bytes=975-2848", 56 | "checksum": "sha256-irIVeq+6d/ZEIg59H1sjgVycwhfmwYwD79+Gv5DJXb0=" 57 | } 58 | }, 59 | { 60 | "name": "pretend-baselayout", 61 | "url": "./testdata/packages/aarch64/pretend-baselayout-1.0.0-r0.apk", 62 | "version": "1.0.0-r0", 63 | "architecture": "aarch64", 64 | "signature": { 65 | "range": "bytes=0-683", 66 | "checksum": "sha1-HFPvVMM/+d7Rm+XbuxpEyWM4wR4=" 67 | }, 68 | "control": { 69 | "range": "bytes=684-961", 70 | "checksum": "sha1-IhHX8PA1MiPcnOa5ig/Wjr/qhbU=" 71 | }, 72 | "data": { 73 | "range": "bytes=962-2900", 74 | "checksum": "sha256-HxWjXtCvMsSYOmhE5OUPz9KQ4kpm0Ywg17OzSf3Ikl4=" 75 | } 76 | }, 77 | { 78 | "name": "replayout", 79 | "url": "./testdata/packages/aarch64/replayout-1.0.0-r0.apk", 80 | "version": "1.0.0-r0", 81 | "architecture": "aarch64", 82 | "signature": { 83 | "range": "bytes=0-688", 84 | "checksum": "sha1-1N7VDS5GSkkT4bzQ5ZJ5VzWiR6w=" 85 | }, 86 | "control": { 87 | "range": "bytes=689-977", 88 | "checksum": "sha1-twZgE0XMokZ89rlCxtGjp+XgtIc=" 89 | }, 90 | "data": { 91 | "range": "bytes=978-2849", 92 | "checksum": "sha256-Mkciu4YKl5ibdvTTg7BQOBSN2xVT1GC5VBZ3/D4qz24=" 93 | } 94 | } 95 | ] 96 | } 97 | } -------------------------------------------------------------------------------- /internal/cli/testdata/apko.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - ./testdata/melange.rsa.pub 4 | repositories: 5 | - ./testdata/packages 6 | packages: 7 | - replayout 8 | 9 | entrypoint: 10 | command: /bin/sh -l 11 | 12 | archs: 13 | - x86_64 14 | - aarch64 15 | -------------------------------------------------------------------------------- /internal/cli/testdata/bar/bar.txt: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /internal/cli/testdata/base_image.apko.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - ./melange.rsa.pub 4 | - ./private_packages/private_pkg_key.rsa.pub 5 | repositories: 6 | - ./packages 7 | - ./private_packages/packages 8 | packages: 9 | - package-y 10 | 11 | entrypoint: 12 | command: /bin/sh -l 13 | 14 | archs: 15 | - x86_64 16 | - aarch64 -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/2580c204e7b254c8988c5cd1840cd58246afafb2479190b6cb3f448341003a5f: -------------------------------------------------------------------------------- 1 | {"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:783b8b05724ae7998917558527ef930f1442af2f071850913fc406992e44606c"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"]}} -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/2ef91a9967f2e1759ea49a8c01cf6a45dd9f9af71fe09bcf2b86175bc4a71314: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":524,"digest":"sha256:2580c204e7b254c8988c5cd1840cd58246afafb2479190b6cb3f448341003a5f"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":4126,"digest":"sha256:bf74ddaf55d32ec9672a0a40efc6cb1bf0a167763c18fc22586c8a301167822f"}]} -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/583625b6164fff3b017f62b9fcd60cb53fff18a7e89ee538212134a13fc29fb1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/base_image/blobs/sha256/583625b6164fff3b017f62b9fcd60cb53fff18a7e89ee538212134a13fc29fb1 -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/5a99438a9ced8193f1d71209d0b558fdc0b184aee5cf258e5f7aa9a6ab0f0671: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":524,"digest":"sha256:65eba664f408f8a2b2cf6b190255c8b0e8cb16a85d773089ae184ac35d782464"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":4123,"digest":"sha256:583625b6164fff3b017f62b9fcd60cb53fff18a7e89ee538212134a13fc29fb1"}]} -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/65eba664f408f8a2b2cf6b190255c8b0e8cb16a85d773089ae184ac35d782464: -------------------------------------------------------------------------------- 1 | {"architecture":"arm64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:2888aac57b90cf66093aa48092bf1f1f1b1bdb85bde8601a5f8cf0f06c814763"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"]}} -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/blobs/sha256/bf74ddaf55d32ec9672a0a40efc6cb1bf0a167763c18fc22586c8a301167822f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/base_image/blobs/sha256/bf74ddaf55d32ec9672a0a40efc6cb1bf0a167763c18fc22586c8a301167822f -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/index.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":402,"digest":"sha256:2ef91a9967f2e1759ea49a8c01cf6a45dd9f9af71fe09bcf2b86175bc4a71314","platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":402,"digest":"sha256:5a99438a9ced8193f1d71209d0b558fdc0b184aee5cf258e5f7aa9a6ab0f0671","platform":{"architecture":"arm64","os":"linux"}}]} -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/metadata/aarch64/APKINDEX: -------------------------------------------------------------------------------- 1 | P:pretend-baselayout 2 | V:1.0.0-r0 3 | A:aarch64 4 | L:MIT 5 | T:replacement baselayout 6 | o:pretend-baselayout 7 | m: 8 | U: 9 | D: 10 | p: 11 | c: 12 | i:[] 13 | t:0 14 | S:2328 15 | I:2900 16 | k:0 17 | C:Q1IhHX8PA1MiPcnOa5ig/Wjr/qhbU= 18 | F:etc 19 | M:501:20:0755 20 | R:os-release 21 | a:501:20:0644 22 | Z:Q1KhFxtJsuPVmrrXKMewSAc2/3kus= 23 | F:var 24 | M:501:20:0755 25 | F:var/lib 26 | M:501:20:0755 27 | F:var/lib/db 28 | M:501:20:0755 29 | F:var/lib/db/sbom 30 | M:501:20:0755 31 | R:pretend-baselayout-1.0.0-r0.spdx.json 32 | a:501:20:0644 33 | Z:Q18qKD7+5VFehz4FrbqJTLYycFsUA= 34 | 35 | P:package-x 36 | V:1.0.0-r0 37 | A:aarch64 38 | L:MIT 39 | T:package x 40 | o:package-x 41 | m: 42 | U: 43 | D:pretend-baselayout 44 | p: 45 | c: 46 | i:[] 47 | t:0 48 | S:2261 49 | I:2746 50 | k:0 51 | C:Q19EfeYO8EvkWtw2p6+d3RYHK8DMU= 52 | F:var 53 | M:501:20:0755 54 | F:var/lib 55 | M:501:20:0755 56 | F:var/lib/db 57 | M:501:20:0755 58 | F:var/lib/db/sbom 59 | M:501:20:0755 60 | R:package-x-1.0.0-r0.spdx.json 61 | a:501:20:0644 62 | Z:Q10JgdqNuHdTtTP2Xb2XKejNgte6k= 63 | F:x 64 | M:501:20:0755 65 | R:hello.txt 66 | a:501:20:0644 67 | Z:Q1qO7DClstcbyJAXX1s2Hrso18VKg= 68 | 69 | P:package-y 70 | V:1.0.0-r0 71 | A:aarch64 72 | L:MIT 73 | T:package y 74 | o:package-y 75 | m: 76 | U: 77 | D:package-x 78 | p: 79 | c: 80 | i:[] 81 | t:0 82 | S:2266 83 | I:2746 84 | k:0 85 | C:Q10Kit8VfVk/eXBIzHVl3QAg2k/Jg= 86 | F:var 87 | M:501:20:0755 88 | F:var/lib 89 | M:501:20:0755 90 | F:var/lib/db 91 | M:501:20:0755 92 | F:var/lib/db/sbom 93 | M:501:20:0755 94 | R:package-y-1.0.0-r0.spdx.json 95 | a:501:20:0644 96 | Z:Q1e+t8INbo1quW7BDfK4qW8ahjSnQ= 97 | F:y 98 | M:501:20:0755 99 | R:hello.txt 100 | a:501:20:0644 101 | Z:Q1qO7DClstcbyJAXX1s2Hrso18VKg= -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/metadata/x86_64/APKINDEX: -------------------------------------------------------------------------------- 1 | P:pretend-baselayout 2 | V:1.0.0-r0 3 | A:x86_64 4 | L:MIT 5 | T:replacement baselayout 6 | o:pretend-baselayout 7 | m: 8 | U: 9 | D: 10 | p: 11 | c: 12 | i:[] 13 | t:0 14 | S:2330 15 | I:2899 16 | k:0 17 | C:Q1cs+Hlyu5sY+1mmwKPedcRWj8E24= 18 | F:etc 19 | M:501:20:0755 20 | R:os-release 21 | a:501:20:0644 22 | Z:Q1KhFxtJsuPVmrrXKMewSAc2/3kus= 23 | F:var 24 | M:501:20:0755 25 | F:var/lib 26 | M:501:20:0755 27 | F:var/lib/db 28 | M:501:20:0755 29 | F:var/lib/db/sbom 30 | M:501:20:0755 31 | R:pretend-baselayout-1.0.0-r0.spdx.json 32 | a:501:20:0644 33 | Z:Q1Y2MFMS5BTeVjVOoqQntuo+V9Y7M= 34 | 35 | P:package-x 36 | V:1.0.0-r0 37 | A:x86_64 38 | L:MIT 39 | T:package x 40 | o:package-x 41 | m: 42 | U: 43 | D:pretend-baselayout 44 | p: 45 | c: 46 | i:[] 47 | t:0 48 | S:2274 49 | I:2745 50 | k:0 51 | C:Q1Vl3vU8cV22CofaOpqDJHyDPEzH8= 52 | F:var 53 | M:501:20:0755 54 | F:var/lib 55 | M:501:20:0755 56 | F:var/lib/db 57 | M:501:20:0755 58 | F:var/lib/db/sbom 59 | M:501:20:0755 60 | R:package-x-1.0.0-r0.spdx.json 61 | a:501:20:0644 62 | Z:Q1A6XBnjIPEhLkSv/6BmO6InZrBLU= 63 | F:x 64 | M:501:20:0755 65 | R:hello.txt 66 | a:501:20:0644 67 | Z:Q1qO7DClstcbyJAXX1s2Hrso18VKg= 68 | 69 | P:package-y 70 | V:1.0.0-r0 71 | A:x86_64 72 | L:MIT 73 | T:package y 74 | o:package-y 75 | m: 76 | U: 77 | D:package-x 78 | p: 79 | c: 80 | i:[] 81 | t:0 82 | S:2258 83 | I:2745 84 | k:0 85 | C:Q1GsmJ2snap2o2kZ+vvMmuEc+bSnM= 86 | F:var 87 | M:501:20:0755 88 | F:var/lib 89 | M:501:20:0755 90 | F:var/lib/db 91 | M:501:20:0755 92 | F:var/lib/db/sbom 93 | M:501:20:0755 94 | R:package-y-1.0.0-r0.spdx.json 95 | a:501:20:0644 96 | Z:Q1meqt/7LqNz2HmUhFB53t8Ro0LEo= 97 | F:y 98 | M:501:20:0755 99 | R:hello.txt 100 | a:501:20:0644 101 | Z:Q1qO7DClstcbyJAXX1s2Hrso18VKg= -------------------------------------------------------------------------------- /internal/cli/testdata/base_image/oci-layout: -------------------------------------------------------------------------------- 1 | { 2 | "imageLayoutVersion": "1.0.0" 3 | } -------------------------------------------------------------------------------- /internal/cli/testdata/empty-layering.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - ./testdata/melange.rsa.pub 4 | repositories: 5 | - ./testdata/packages 6 | packages: 7 | - replayout 8 | 9 | entrypoint: 10 | command: /bin/sh -l 11 | 12 | archs: 13 | - x86_64 14 | - aarch64 15 | 16 | # Empty layering configuration 17 | layering: {} -------------------------------------------------------------------------------- /internal/cli/testdata/foo/foo.txt: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/05722e7f4346cd28e43fd2605ece5517af1ed16e22b24aabba500a51a296ce7c: -------------------------------------------------------------------------------- 1 | {"architecture":"arm64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:fd94a07c216012e0e61da443be98b8d6a094824d914c5d094e8fd3faa34566d0"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}} -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/13233c203a5e591839f93061a9153446bc2ac0355710f0e80b2a7a47923e8fcd: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:7ed5a4e62d3da49c1dbbeec81f803d22714c81117a2229f7234463a0c4a8dd0b"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":2958,"digest":"sha256:6db738b7b040fcbc8d58cd6a09ac06d3a1ccbd132e29cffe33f7e0499b080d03"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/5dc95810fb12276e75b696b9bce59966c34483797baca0518e3aa6417fe06700: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/golden/blobs/sha256/5dc95810fb12276e75b696b9bce59966c34483797baca0518e3aa6417fe06700 -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/6db738b7b040fcbc8d58cd6a09ac06d3a1ccbd132e29cffe33f7e0499b080d03: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/golden/blobs/sha256/6db738b7b040fcbc8d58cd6a09ac06d3a1ccbd132e29cffe33f7e0499b080d03 -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/7ed5a4e62d3da49c1dbbeec81f803d22714c81117a2229f7234463a0c4a8dd0b: -------------------------------------------------------------------------------- 1 | {"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:33c4bd8677e3684ac4b9dbfdd5c8cc258e84c0460d5edc7147bbbd8b4d752076"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}} -------------------------------------------------------------------------------- /internal/cli/testdata/golden/blobs/sha256/80121a5654f4a873b122ddb556aba614296b156ac3856e102dbf8a33711e23ab: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:05722e7f4346cd28e43fd2605ece5517af1ed16e22b24aabba500a51a296ce7c"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":2962,"digest":"sha256:5dc95810fb12276e75b696b9bce59966c34483797baca0518e3aa6417fe06700"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} -------------------------------------------------------------------------------- /internal/cli/testdata/golden/index.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":476,"digest":"sha256:13233c203a5e591839f93061a9153446bc2ac0355710f0e80b2a7a47923e8fcd","platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":476,"digest":"sha256:80121a5654f4a873b122ddb556aba614296b156ac3856e102dbf8a33711e23ab","platform":{"architecture":"arm64","os":"linux"}}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}} -------------------------------------------------------------------------------- /internal/cli/testdata/golden/oci-layout: -------------------------------------------------------------------------------- 1 | { 2 | "imageLayoutVersion": "1.0.0" 3 | } -------------------------------------------------------------------------------- /internal/cli/testdata/image_on_top.apko.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v1", 3 | "config": { 4 | "name": "testdata/image_on_top.apko.yaml", 5 | "checksum": "sha256-eQuz6VtB0U8NsZA8pbhyoR3HZSRULKXbiv1OzNvpZUk=" 6 | }, 7 | "contents": { 8 | "keyring": [ 9 | { 10 | "name": "./testdata/melange.rsa.pub", 11 | "url": "./testdata/melange.rsa.pub" 12 | } 13 | ], 14 | "build_repositories": [], 15 | "repositories": [ 16 | { 17 | "name": "./testdata/packages/x86_64", 18 | "url": "./testdata/packages/x86_64/APKINDEX.tar.gz", 19 | "architecture": "x86_64" 20 | }, 21 | { 22 | "name": "./testdata/packages/aarch64", 23 | "url": "./testdata/packages/aarch64/APKINDEX.tar.gz", 24 | "architecture": "aarch64" 25 | } 26 | ], 27 | "packages": [ 28 | { 29 | "name": "replayout", 30 | "url": "./testdata/packages/x86_64/replayout-1.0.0-r0.apk", 31 | "version": "1.0.0-r0", 32 | "architecture": "x86_64", 33 | "signature": { 34 | "range": "bytes=0-647", 35 | "checksum": "sha1-ZrPCeQ4XeDjZSQw+IhJ4g4BcUlo=" 36 | }, 37 | "control": { 38 | "range": "bytes=648-1589", 39 | "checksum": "sha1-IvTcfj6zzLipr9akZ+YRTIyQCr8=" 40 | }, 41 | "data": { 42 | "range": "bytes=1590-2786", 43 | "checksum": "sha256-IIzbGjwv4H9h6N1bEbF8p4cqkV0Ex54sXEsvf6txnEo=" 44 | }, 45 | "checksum": "Q1IvTcfj6zzLipr9akZ+YRTIyQCr8=" 46 | }, 47 | { 48 | "name": "replayout", 49 | "url": "./testdata/packages/aarch64/replayout-1.0.0-r0.apk", 50 | "version": "1.0.0-r0", 51 | "architecture": "aarch64", 52 | "signature": { 53 | "range": "bytes=0-646", 54 | "checksum": "sha1-1ifrimC4bUlo4O6aCGXcjOKAcTo=" 55 | }, 56 | "control": { 57 | "range": "bytes=647-1586", 58 | "checksum": "sha1-SWYSZF3dGLrN8kebGjOBfDH6vG4=" 59 | }, 60 | "data": { 61 | "range": "bytes=1587-2787", 62 | "checksum": "sha256-U85iWddHApXsVosa4S0srhkr3SSlzuoBrEpZg5f1irQ=" 63 | }, 64 | "checksum": "Q1SWYSZF3dGLrN8kebGjOBfDH6vG4=" 65 | } 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/cli/testdata/image_on_top.apko.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | baseimage: 3 | image: ./testdata/base_image/ 4 | apkindex: ./testdata/base_image/metadata/ 5 | keyring: 6 | - ./testdata/melange.rsa.pub 7 | repositories: 8 | - ./testdata/packages 9 | packages: 10 | - replayout 11 | 12 | archs: 13 | - x86_64 14 | - aarch64 -------------------------------------------------------------------------------- /internal/cli/testdata/layering.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | keyring: 3 | - ./testdata/melange.rsa.pub 4 | build_repositories: 5 | - ./packages 6 | repositories: 7 | - ./testdata/packages 8 | packages: 9 | - replayout 10 | 11 | entrypoint: 12 | command: /bin/sh -l 13 | 14 | archs: 15 | - x86_64 16 | - aarch64 17 | 18 | layering: 19 | strategy: origin 20 | budget: 2 21 | -------------------------------------------------------------------------------- /internal/cli/testdata/melange.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuE9aHvpBoe7HNnWxhp2p 3 | x+HGjbPhzP0CyQYMNMcHjd76UcaPwWNTVqJI8JMT2u72mPsEXpC+J6KqjIggJoOa 4 | QYg87oYEdthoJZjyDaKzezNZKndzVQkg4RfQPiCPQkm4r9v6So0MCDa4rwRYnqrf 5 | s3Je9/wi8B/wG1sz7P4wOG23djxpORDsMI9CyZhnAKfNg/uqmF/sxEwOL4FaDZMg 6 | 0oo7Kx3FjtqqKTV8uFbCDsTxV3CR0pm3WJdX9TNfjXLXfp2a6QDYk1JoYl++UUHK 7 | +7izMXro6xgY7OPT+F48/YNYxS/ciH90DmN7ysJnQ2otZHSqhOkJV4UrYCKBHY55 8 | XjpDe7+nmwXaqVyAlCS6pDqHeYUUpYTpnv4b9bbst9NtYPRY8W00Oc5Cs3KdHeV6 9 | LqvHAGwNTZziv91UTpi0hMf27I+MLaXx+jNWm5j40a+ZyswLAQSLI+u3LxfPlHda 10 | lhpaQw+CoM3ucX9rcnJaBgowXclDAAvRNIj7EJNW5sk+3SbpmKKDkrD7Cl0rMSrd 11 | 1dixDZZPzA8UtwheNTmV+I+0r1kQMcNYcB8iUKsoWIpaur0CBCww02WTHpOMcMGw 12 | +rVr/wzPP3Iqo1+EVrzVI5kSnZ7VLRtO5VdMLw0PlIK4ShJ+X5OtVtWnJ9jIiLaI 13 | se0tr8lpqU40eV862X+jKz0CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /internal/cli/testdata/packages/aarch64/APKINDEX.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/aarch64/APKINDEX.tar.gz -------------------------------------------------------------------------------- /internal/cli/testdata/packages/aarch64/pretend-baselayout-1.0.0-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/aarch64/pretend-baselayout-1.0.0-r0.apk -------------------------------------------------------------------------------- /internal/cli/testdata/packages/aarch64/replayout-1.0.0-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/aarch64/replayout-1.0.0-r0.apk -------------------------------------------------------------------------------- /internal/cli/testdata/packages/melange.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuE9aHvpBoe7HNnWxhp2p 3 | x+HGjbPhzP0CyQYMNMcHjd76UcaPwWNTVqJI8JMT2u72mPsEXpC+J6KqjIggJoOa 4 | QYg87oYEdthoJZjyDaKzezNZKndzVQkg4RfQPiCPQkm4r9v6So0MCDa4rwRYnqrf 5 | s3Je9/wi8B/wG1sz7P4wOG23djxpORDsMI9CyZhnAKfNg/uqmF/sxEwOL4FaDZMg 6 | 0oo7Kx3FjtqqKTV8uFbCDsTxV3CR0pm3WJdX9TNfjXLXfp2a6QDYk1JoYl++UUHK 7 | +7izMXro6xgY7OPT+F48/YNYxS/ciH90DmN7ysJnQ2otZHSqhOkJV4UrYCKBHY55 8 | XjpDe7+nmwXaqVyAlCS6pDqHeYUUpYTpnv4b9bbst9NtYPRY8W00Oc5Cs3KdHeV6 9 | LqvHAGwNTZziv91UTpi0hMf27I+MLaXx+jNWm5j40a+ZyswLAQSLI+u3LxfPlHda 10 | lhpaQw+CoM3ucX9rcnJaBgowXclDAAvRNIj7EJNW5sk+3SbpmKKDkrD7Cl0rMSrd 11 | 1dixDZZPzA8UtwheNTmV+I+0r1kQMcNYcB8iUKsoWIpaur0CBCww02WTHpOMcMGw 12 | +rVr/wzPP3Iqo1+EVrzVI5kSnZ7VLRtO5VdMLw0PlIK4ShJ+X5OtVtWnJ9jIiLaI 13 | se0tr8lpqU40eV862X+jKz0CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /internal/cli/testdata/packages/x86_64/APKINDEX.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/x86_64/APKINDEX.tar.gz -------------------------------------------------------------------------------- /internal/cli/testdata/packages/x86_64/pretend-baselayout-1.0.0-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/x86_64/pretend-baselayout-1.0.0-r0.apk -------------------------------------------------------------------------------- /internal/cli/testdata/packages/x86_64/replayout-1.0.0-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/internal/cli/testdata/packages/x86_64/replayout-1.0.0-r0.apk -------------------------------------------------------------------------------- /internal/cli/testdata/pretend-baselayout.melange.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: pretend-baselayout 3 | version: 1.0.0 4 | epoch: 0 5 | description: "replacement baselayout" 6 | copyright: 7 | - license: MIT 8 | 9 | environment: 10 | contents: 11 | packages: 12 | - busybox 13 | 14 | pipeline: 15 | - name: Generate /etc/os-release 16 | runs: | 17 | mkdir -p ${{targets.destdir}}/etc 18 | cat >${{targets.destdir}}/etc/os-release <${{targets.destdir}}/x/hello.txt <${{targets.destdir}}/y/hello.txt < /dev/null && pwd) 3 | 4 | set -e -x 5 | 6 | mkdir -p "${SCRIPT_DIR}/top_image.new" 7 | ( 8 | cd "${SCRIPT_DIR}/.." 9 | go run "../.." build \ 10 | --include-paths="${SCRIPT_DIR}/.." \ 11 | --lockfile=./testdata/image_on_top.apko.lock.json \ 12 | ./testdata/image_on_top.apko.yaml \ 13 | topimage \ 14 | ./testdata/top_image.new/ 15 | rm -rf "${SCRIPT_DIR}/top_image" 16 | mv "${SCRIPT_DIR}/top_image.new" "${SCRIPT_DIR}/top_image" 17 | ) 18 | -------------------------------------------------------------------------------- /internal/cli/testdata/replayout.melange.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: replayout 3 | version: 1.0.0 4 | epoch: 0 5 | description: "replacement baselayout" 6 | copyright: 7 | - license: MIT 8 | dependencies: 9 | replaces: 10 | - pretend-baselayout 11 | runtime: 12 | - pretend-baselayout 13 | 14 | environment: 15 | contents: 16 | packages: 17 | - busybox 18 | 19 | pipeline: 20 | - name: Generate /etc/os-release 21 | runs: | 22 | mkdir -p ${{targets.destdir}}/etc 23 | cat >${{targets.destdir}}/etc/os-release < 12 | t:1600096848 13 | c:af13bd168c9d86ede4ad1be5c4ceac79253a7e26 14 | D:so:libc.musl-x86_64.so.1 15 | 16 | C:Q1Pi7+Lp0TdU9DNxeZKvFbOSjmncw= 17 | P:b-pkg 18 | V:1.1.1-r1 19 | A:x86_64 20 | S:5243 21 | I:11392 22 | T:Another package 23 | U:http://b.package.org 24 | L:Apache-2.0 25 | o:b-pkg 26 | m:maintainer 27 | t:1600096848 28 | c:af13bd168c9d86ede4ad1be5c4ceac79253a7e26 29 | D:so:libc.musl-x86_64.so.1 30 | 31 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/extracted/DESCRIPTION: -------------------------------------------------------------------------------- 1 | v20210402-2123-gabcdef 2 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/hello-0.1.0-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/apk/testdata/hello-0.1.0-r0.apk -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/hello-wolfi-2.12.1-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/apk/testdata/hello-wolfi-2.12.1-r0.apk -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/replaces/melange.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: replaces 3 | version: 0.0.1 4 | description: testdata with multiple replaces 5 | dependencies: 6 | replaces: 7 | - foo 8 | - bar 9 | 10 | environment: 11 | contents: 12 | repositories: 13 | - https://dl-cdn.alpinelinux.org/alpine/edge/main 14 | packages: 15 | - alpine-baselayout-data 16 | - busybox 17 | 18 | pipeline: 19 | - runs: | 20 | mkdir -p "${{targets.destdir}}" 21 | echo "hello" > "${{targets.destdir}}/hello" 22 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/replaces/replaces-0.0.1-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/apk/testdata/replaces/replaces-0.0.1-r0.apk -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/root/usr/lib/apk/db/triggers: -------------------------------------------------------------------------------- 1 | Q1Meo+LHGPSi3uY9gIouEVb9z8Fbo= /bin /usr/bin /sbin /usr/sbin /lib/modules/* 2 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/rsa256-signed/APKINDEX.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/apk/testdata/rsa256-signed/APKINDEX.tar.gz -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/rsa256-signed/alpine-baselayout-3.2.0-r23.apk: -------------------------------------------------------------------------------- 1 | ../alpine-316/alpine-baselayout-3.2.0-r23.apk -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/rsa256-signed/rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | # get one package 5 | ln -sf ../alpine-316/alpine-baselayout-3.2.0-r23.apk 6 | 7 | # generate test key 8 | melange keygen test-rsa256.rsa 9 | 10 | # generate APKINDEX 11 | melange index -o APKINDEX.tar.gz *.apk 12 | 13 | # sign with RSA256 14 | # Using abuild from https://gitlab.alpinelinux.org/alpine/abuild/-/merge_requests/290 15 | PATH=$PATH:~/upstream/abuild ABUILD_SHAREDIR=~/upstream/abuild CBUILD=aarch64 abuild-sign --private test-rsa256.rsa --type RSA256 APKINDEX.tar.gz 16 | 17 | rm -f test-rsa256.rsa 18 | 19 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/rsa256-signed/test-rsa256.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArPL3OE2edJeOFFvd3iXS 3 | n8+VP4LDydDaqIVJYsMZvbeASmxxgmo7PTCnr0N8hzDrUEn9D4Tb2vK6HGkMh89v 4 | RkC8EXCKKltAAHOYG40yUDhnrkIzbnUl1Y27I69I9xIojFBXhB+nUckDCmqEhM7h 5 | dGuCKZVr+ABXEgpPy4jua0OXXKmgGGFNnMGXg5BJ8xQS6NjAje1clpqgTcW4vMDi 6 | VCfosZtPEL1uh7MqoKa1f63PuegQhlXI5q++LQL4O7aQKdZvk9RFV2PyIIgLE2Dr 7 | WVLqCI2k2veoYJiz83oiixcxiHVqVqyrnLIFPT+F4dbH6lC4JhcIVK4Vvg+uY2/z 8 | gp090qRbje3H/VV9SsKpwj7ZbzSz4H5zyafs0ONDVvTIzmz3M1vLS+Au8NqRkGNj 9 | dxV4ZyYalq7BWrI51SKt1EyWQxWwTQcwIaaAMUIqOsc3I6qIvDRoNTY+ORTNwrud 10 | +6Ve5h3bP9UyEHnSHQBHOEzQzlLDawCUo4RcyPsFfMqhCrCBM1IAKCFm4aeaIr7Q 11 | 0x+r1NFiZkOsW2JrbKFaA4swQdPGtirtxvcyq4HBP1XW1mlujKKFV64nByfMEaRs 12 | UUcx14yovgv54uwNX4c8BRSYAuU/enW7jrdPKpLWOzKuKcRg8f+sIpblF2m5Fbqp 13 | guyM+Ks3c29KlRf3iX35Gt0CAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/signing/APKINDEX.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/apk/testdata/signing/APKINDEX.tar.gz -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/signing/keys/chainguard-0106f58bac88057c2ff5c2829850df492717a876ed700443550353c7ab23f5a0.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2TxPZPC1Cdb647P0kvi5 3 | wtwcoaFCOGHspL3RmmD2idG2FNZc/rpwyIp2bnprFvOCWm7bKAEl/9JDfAGm7DAP 4 | q/EC7UeJ9yx03jY1I7+Oj1/1UUIyuvfDwyiMxEwcABaivAn9WhYZ5YeZKK8pUYoX 5 | hOPNqgI+N3BY1H+hgg3xgTrzWmfRSsvTEhSqoLHdl4qbsM5EE/T5W7u+96y9J2vn 6 | M89rUP557K3n1QKGzTIGjlesFQd8y+uH0TdAe6AFmxFpftdXRu4KInjdrDOArfBO 7 | cKLB0gHOERkd08JVMWYHd6Ne7BfWpwxmge371GgTj8XYOJrkAj3cg8ked/51Maw0 8 | FLt4aErQR6y02QIluGZ4gwbIZ8y+iBWvoh8egiab3Whr3SwGuX6X8WJbOgKLCyGK 9 | CmrxgPO/ahS4XUWtuTp3woIZBnnHGOBdHjRARkl9PdBgN2HhZPS9vj8rrIqbTp3c 10 | dmqhcsjsgmss6Sb3rOcOL3N3V8+dMiD7VZXeIgVocOpRtLJGQFCupjAfmz5ub4t5 11 | lBI2EgbR9LdSg9mrFoYbG11sU6MLa0iPMA8gU3nG4dzLDpsF9PbXy6sfBXXc8W25 12 | tfwZnV4wHouWEhiQ9Hfc4NOGcs+01Zwg9CZRS2SBKC9iqUPsz3RaTAKWsS/QNTND 13 | Zy9znhL5yGrbHaq2BOlnVqMCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/signing/keys/chainguard-60912bbc46bfc8ed6bda0b50db3a8a5f3c4344d6bc8549ec9b84d96140b475d1.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAizNP/QKfB5NKlVBba6dX 3 | Jzz0/icPE++eM3aAtCARwlAKyRm3ivWM5sAz88lwHFjMAQV0QPD3hDgswcAExTXa 4 | EPN8M0RroxxnWuJLGhz99ONaGOLo66GDkcdBaaZIinXoWmhqg1zcequ6dLDOGArm 5 | ZR99egaBFKyQHvBYHIl5dQ1yT8us/m1Yfb7DlERcU2lf3lFhLDrJsBgVw5QYa5h+ 6 | aSneA/B2ZB1BwfyE3F5+UUn0qWRy3Yg526oaWpjjwCaPVq4yaJfg4XuMS82wdVm9 7 | G6awP1PE0EWwgkcc8z8w7BmxFhdCRt0VNjmept14tJwiU0d81j1HPHsIkqWr2TBA 8 | f7Wwp34+ByfA7iHbKXFPgK1MKmNzyANoyPaPRo+x+MU8D2AqUBslKOKrhckyd4tQ 9 | ch3SyaMKMa7hc0PWnFrCCtDTmNZQaquNrUWnbbMtdkjCA/is//W+Lqn+O6BjmR9U 10 | yztR26l2OwGMu6uf3CMUADX/ra4k2BuxSnaPTgm7M19AAzXSnICa+aupgFsIfx1w 11 | UyGlq8dvCz8bnipXW4EbTWTSPdFrxQl/LQUB5w82kMorML1TQEpSLjoSmnhNg+rh 12 | rqIsjp7fwEnX/qTbiwUhgcN1fyLkGGv/UV/H62WcclStKiRHp+ND0ePMABefA+cv 13 | f9tYynbOV78qct+dQKkQ17kCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /pkg/apk/apk/testdata/signing/keys/chainguard-61cb6bccd1f584b007db7be51ebce2b0530a54cc5a94e7650b570d113d537cf3.rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwlV2kG9xGtkVWc/dS9nC 3 | Qqef/hx0AysYfrMFetY+mZ0MPvELqOaDJgtjlbMM6w/ujqDtScxbnNNf5/vY/wBh 4 | rHTeKU+oDlkRTsh8ZxKINFfGGSDxR2pKkvPZeVzxdhTNEPK9ZggbFHe5RhXUT9iK 5 | CNgF+Xa3p5A1Yi8zR6zEFJ5EDBMnTMagV2ueLfJrpFMm/4hBn6zWtjyDhDPVZyTD 6 | DyQc2/FIDXM1tJONWDT8wO2xuHf7xKGr6lO/CZca5Pnd/QS5jODoNbTo9VvG+oQt 7 | mRsWsdVFst0vS54s3mchtuAB2NXnWAZZuux9uYPgvO8eqI0RLqCKX7VLJ8tGOTmL 8 | 3dblTzKbKbVcy4uHsyH2mZPvPUTO+ck7c58iSeUrOFV50B6zqynAXgCrDo3XMjpI 9 | mLC26LRMqF/Q/RWc9R0K+ZgiLQ7ootyOJILPSR69RhgdQJVt7/gz55dM89yoq/B9 10 | U2V+1mv0rBQmmNgCM1U3QdgUv5WeIm4Ed2avKZBwJLBVR5AxlY9xlwJ7sEU4Ms2t 11 | cEblWhqDze/sSsjCYOAmD4/M/90OnBelg+/BU2jOD4nqQdEQDOZdo+EUVeCIuPwD 12 | kz8kTv8pPtLjKI0jRhYLn2dhg/vTIDJf19iXexGQXOyK/rNwi41i+9snypVb8YSe 13 | cWTnq+m2CFOiXFsDWQh7RsUCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /pkg/apk/apk/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apk 16 | 17 | import ( 18 | "archive/tar" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "slices" 23 | "strings" 24 | ) 25 | 26 | func uniqify[T comparable](s []T) []T { 27 | seen := make(map[T]struct{}, len(s)) 28 | uniq := make([]T, 0, len(s)) 29 | for _, v := range s { 30 | if _, ok := seen[v]; ok { 31 | continue 32 | } 33 | 34 | uniq = append(uniq, v) 35 | seen[v] = struct{}{} 36 | } 37 | 38 | return uniq 39 | } 40 | 41 | func controlValue(controlTar io.Reader, want ...string) (map[string][]string, error) { 42 | tr := tar.NewReader(controlTar) 43 | for { 44 | header, err := tr.Next() 45 | if errors.Is(err, io.EOF) { 46 | return nil, fmt.Errorf("control file not found") 47 | } 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | // ignore .PKGINFO as it is not a script 53 | if header.Name != ".PKGINFO" { 54 | continue 55 | } 56 | 57 | b, err := io.ReadAll(tr) 58 | if err != nil { 59 | return nil, fmt.Errorf("unable to read .PKGINFO from control tar.gz file: %w", err) 60 | } 61 | mapping := map[string][]string{} 62 | lines := strings.Split(string(b), "\n") 63 | for _, line := range lines { 64 | parts := strings.Split(line, "=") 65 | if len(parts) != 2 { 66 | continue 67 | } 68 | key := strings.TrimSpace(parts[0]) 69 | if !slices.Contains(want, key) { 70 | continue 71 | } 72 | 73 | values, ok := mapping[key] 74 | if !ok { 75 | values = []string{} 76 | } 77 | 78 | value := strings.TrimSpace(parts[1]) 79 | values = append(values, value) 80 | 81 | mapping[key] = values 82 | } 83 | return mapping, nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/apk/apk/world.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apk 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "io" 21 | "path/filepath" 22 | "sort" 23 | "strings" 24 | 25 | "github.com/chainguard-dev/clog" 26 | ) 27 | 28 | // GetWorld - get list of packages that should be installed, according to /etc/apk/world 29 | func (a *APK) GetWorld() ([]string, error) { 30 | worldFile, err := a.fs.Open(worldFilePath) 31 | if err != nil { 32 | return nil, fmt.Errorf("could not open world file in %s at %s: %w", a.fs, worldFilePath, err) 33 | } 34 | defer worldFile.Close() 35 | worldData, err := io.ReadAll(worldFile) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to read world file: %w", err) 38 | } 39 | return strings.Fields(string(worldData)), nil 40 | } 41 | 42 | // SetWorld sets the list of world packages intended to be installed. 43 | // The base directory of /etc/apk must already exist, i.e. this only works on an initialized APK database. 44 | func (a *APK) SetWorld(ctx context.Context, packages []string) error { 45 | log := clog.FromContext(ctx) 46 | log.Debug("setting apk world") 47 | 48 | // sort them before writing 49 | copied := make([]string, len(packages)) 50 | copy(copied, packages) 51 | sort.Strings(copied) 52 | 53 | data := strings.Join(copied, "\n") + "\n" 54 | 55 | // #nosec G306 -- apk world must be publicly readable 56 | if err := a.fs.WriteFile(filepath.Join("etc", "apk", "world"), 57 | []byte(data), 0o644); err != nil { 58 | return fmt.Errorf("failed to write apk world: %w", err) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/apk/apk/world_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package apk 16 | 17 | import ( 18 | "strings" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | apkfs "chainguard.dev/apko/pkg/apk/fs" 24 | ) 25 | 26 | func TestGetWorld(t *testing.T) { 27 | src := apkfs.NewMemFS() 28 | err := src.MkdirAll("etc/apk", 0o755) 29 | require.NoError(t, err, "unable to mkdir /etc/apk") 30 | packages := []string{"package1", "package2", "package3"} 31 | err = src.WriteFile(worldFilePath, []byte(strings.Join(packages, "\n")), 0o644) 32 | require.NoError(t, err, "unable to write world file") 33 | a, err := New(WithFS(src), WithIgnoreMknodErrors(ignoreMknodErrors)) 34 | require.NoError(t, err, "unable to create APK") 35 | pkgs, err := a.GetWorld() 36 | require.NoError(t, err, "unable to get world packages") 37 | require.Equal(t, strings.Join(packages, " "), strings.Join(pkgs, " "), "expected packages %v, got %v", packages, pkgs) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/apk/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | type successAuth struct{} 11 | 12 | func (s successAuth) AddAuth(_ context.Context, req *http.Request) error { 13 | req.SetBasicAuth("user", "pass") 14 | return nil 15 | } 16 | 17 | type failAuth struct{} 18 | 19 | func (f failAuth) AddAuth(_ context.Context, req *http.Request) error { 20 | return errors.New("failed to add auth") 21 | } 22 | 23 | func TestMultiAuthenticator(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | auths []Authenticator 27 | expectAuth bool 28 | expectErr bool 29 | }{ 30 | { 31 | name: "success auth first", 32 | auths: []Authenticator{successAuth{}, failAuth{}}, 33 | expectAuth: true, 34 | expectErr: false, 35 | }, 36 | { 37 | name: "fail auth first", 38 | auths: []Authenticator{failAuth{}, successAuth{}}, 39 | expectAuth: true, 40 | expectErr: false, 41 | }, 42 | { 43 | name: "all fail auth", 44 | auths: []Authenticator{failAuth{}, failAuth{}}, 45 | expectAuth: false, 46 | expectErr: true, 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | multiAuth := MultiAuthenticator(tt.auths...) 53 | req, _ := http.NewRequest("GET", "http://example.com", nil) 54 | err := multiAuth.AddAuth(context.Background(), req) 55 | 56 | if tt.expectErr && err == nil { 57 | t.Errorf("expected error but got none") 58 | } 59 | if !tt.expectErr && err != nil { 60 | t.Errorf("did not expect error but got: %v", err) 61 | } 62 | 63 | user, pass, ok := req.BasicAuth() 64 | if tt.expectAuth && !ok { 65 | t.Errorf("expected auth but got none") 66 | } 67 | if !tt.expectAuth && ok { 68 | t.Errorf("did not expect auth but got user: %s, pass: %s", user, pass) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/apk/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | 11 | "github.com/hashicorp/go-retryablehttp" 12 | 13 | "chainguard.dev/apko/pkg/apk/apk" 14 | "chainguard.dev/apko/pkg/apk/auth" 15 | ) 16 | 17 | const ( 18 | WolfiAPKRepo = "https://packages.wolfi.dev/os" 19 | ChainguardEnterpriseAPKRepo = "https://apk.cgr.dev/chainguard-private" 20 | ChainguardExtrasAPKRepo = "https://apk.cgr.dev/extra-packages" 21 | ) 22 | 23 | const ( 24 | Aarch64Arch = "aarch64" 25 | X86_64Arch = "x86_64" 26 | ) 27 | 28 | // Client is a client for interacting with an APK package repository. 29 | type Client struct { 30 | httpClient *http.Client 31 | } 32 | 33 | // New creates a new Client, suitable for accessing remote APK indexes and 34 | // packages. 35 | func New(httpClient *http.Client) *Client { 36 | if httpClient == nil { 37 | httpClient = http.DefaultClient 38 | } 39 | rc := retryablehttp.NewClient() 40 | rc.Logger = log.New(io.Discard, "", 0) // Don't log requests at all. 41 | rc.HTTPClient = httpClient 42 | return &Client{httpClient: rc.StandardClient()} 43 | } 44 | 45 | // GetRemoteIndex retrieves the index of APK packages from the specified remote 46 | // repository. 47 | // 48 | // `apkRepo` is the URL of the repository including the protocol, e.g. 49 | // "https://packages.wolfi.dev/os". 50 | // 51 | // `arch` is the architecture of the index, e.g. "x86_64" or "aarch64". 52 | func (c Client) GetRemoteIndex(ctx context.Context, apkRepo, arch string) (*apk.APKIndex, error) { 53 | indexURL := apk.IndexURL(apkRepo, arch) 54 | 55 | u, err := url.Parse(indexURL) 56 | if err != nil { 57 | return nil, fmt.Errorf("parsing %q: %w", indexURL, err) 58 | } 59 | 60 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) 61 | if err != nil { 62 | return nil, fmt.Errorf("GET %q: %w", u.Redacted(), err) 63 | } 64 | if err := auth.DefaultAuthenticators.AddAuth(ctx, req); err != nil { 65 | return nil, fmt.Errorf("error adding auth: %w", err) 66 | } 67 | 68 | resp, err := c.httpClient.Do(req) 69 | if err != nil { 70 | return nil, fmt.Errorf("GET %q: %w", u.Redacted(), err) 71 | } 72 | defer resp.Body.Close() 73 | if resp.StatusCode >= 400 { 74 | return nil, fmt.Errorf("GET %q: status %d: %s", u.Redacted(), resp.StatusCode, resp.Status) 75 | } 76 | 77 | return apk.IndexFromArchive(resp.Body) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/apk/docs/CACHE.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | The apk library has the option to cache apk files when downloading. This can provide dramatic speedups 4 | when installing packages, especially when you have a large number of packages to install. 5 | 6 | This is completely independent of the target FS where you install the packages themselves. You might have multiple 7 | install runs, all using similar packages. For example, if you need `busybox-1.36.2-r0.apk` for multiple installs, 8 | you only would need to download it once. 9 | 10 | The cache is **not** enabled by default. It only is enabled by providing the [WithCache()](./pkg/apk/options.go) option to the `New()` function. 11 | 12 | ## Cache Location 13 | 14 | The cache location will be in any provided directory. If you wish to use the default directory, you can pass `""` to 15 | the `WithCache()` option: 16 | 17 | ```go 18 | a, err := apk.New( 19 | apk.WithCache(""), 20 | ) 21 | ``` 22 | 23 | No `WithCache()` option disables use of the cache. 24 | 25 | Note that the default changes by platform, and is determined by [os.userCacheDir()](https://pkg.go.dev/os#UserCacheDir). 26 | 27 | ## Which Repositories are Cache 28 | 29 | Only remote repositories are cached. Those referenced using local filesystems, e.g. `./packages/foo`, are not cached, as they provide 30 | no value in caching. 31 | 32 | ## Cache Structure 33 | 34 | The cache directory provided is the root of the cache. Underneath that directory you will find the following structure. 35 | 36 | There is a directory for each repository used. Because remote repositories can contain invalid directory characters, 37 | the name is URL-encoded. For example, a repository `https://dl-cdn.alpinelinux.org/alpine/v3.14/main` would be encoded as 38 | `https%3A%2F%2Fdl-cdn.alpinelinux.org%2Falpine%2Fv3.14%2Fmain` 39 | 40 | Underneath each repository directory is a directory that looks identical to the repository. There is a directory for each 41 | architecture, inside of which is an `.apk` file for each package. 42 | 43 | When a file is retrieved, if available, the [etag](https://en.wikipedia.org/wiki/HTTP_ETag) header is saved 44 | alongside the file. if it is available, it is saved in a file `.etag`. 45 | This is used to determine if the file has changed. 46 | 47 | Behaviour if no local etag is available depends on how it was called: 48 | 49 | * `APKINDEX.tar.gz` - we assume that it can change, and thus no etag found locally means always retrieve it. 50 | * `.apk` files - we assume that they do not change, and thus no etag found locally means the file is accepted as is. 51 | -------------------------------------------------------------------------------- /pkg/apk/expandapk/const.go: -------------------------------------------------------------------------------- 1 | package expandapk 2 | 3 | const ( 4 | paxRecordsChecksumKey = "APK-TOOLS.checksum.SHA1" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/apk/expandapk/split.go: -------------------------------------------------------------------------------- 1 | package expandapk 2 | 3 | import ( 4 | "archive/tar" 5 | "bufio" 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "strings" 10 | 11 | "github.com/klauspost/compress/gzip" 12 | ) 13 | 14 | // Split takes an APK reader and splits it into its constituent parts. 15 | // 16 | // If the length of the returned slice is 3, the first part is the signature section. 17 | // If the length of the returned slice is 2, the first part is the control section. 18 | // The last part is the data section. 19 | // 20 | // These values are the compressed gzip streams, and should be decompressed before use. 21 | // 22 | // The signature and control sections are buffered in memory, while the data section is streamed 23 | // from the input reader. 24 | func Split(source io.Reader) ([]io.Reader, error) { 25 | parts := []io.Reader{} 26 | 27 | br := bufio.NewReader(source) 28 | 29 | buf := bytes.Buffer{} 30 | tee := &teeByteReader{r: br, w: &buf} 31 | 32 | gzi, err := gzip.NewReader(tee) 33 | if err != nil { 34 | return nil, fmt.Errorf("creating gzip reader: %w", err) 35 | } 36 | gzi.Multistream(false) 37 | 38 | tr := tar.NewReader(gzi) 39 | hdr, err := tr.Next() 40 | if err != nil { 41 | return nil, fmt.Errorf("reading first tar header: %w", err) 42 | } 43 | 44 | // Handle optional signature section. 45 | if strings.HasPrefix(hdr.Name, ".SIGN.") { 46 | if _, err := io.Copy(io.Discard, gzi); err != nil { 47 | return nil, fmt.Errorf("copying signature stream: %w", err) 48 | } 49 | 50 | parts = append(parts, bytes.NewReader(buf.Bytes())) 51 | 52 | // Use a new buffer for the control section. 53 | buf = bytes.Buffer{} 54 | tee.w = &buf 55 | 56 | if err := gzi.Reset(tee); err != nil { 57 | return nil, fmt.Errorf("resetting gzip reader after signature: %w", err) 58 | } 59 | gzi.Multistream(false) 60 | } 61 | 62 | // There should always be a control section. 63 | if _, err := io.Copy(io.Discard, gzi); err != nil { 64 | return nil, fmt.Errorf("copying signature stream: %w", err) 65 | } 66 | 67 | parts = append(parts, bytes.NewReader(buf.Bytes())) 68 | 69 | if err := gzi.Close(); err != nil { 70 | return nil, fmt.Errorf("closing gzip reader: %w", err) 71 | } 72 | 73 | // And the rest is the data section. 74 | parts = append(parts, br) 75 | 76 | return parts, nil 77 | } 78 | 79 | // like io.TeeReader but also implements io.ByteReader for gzip. 80 | // 81 | // From gzip.Reader.Multistream: 82 | // 83 | // > If the underlying reader implements io.ByteReader, 84 | // > it will be left positioned just after the gzip stream. 85 | type teeByteReader struct { 86 | r interface { 87 | io.Reader 88 | io.ByteReader 89 | } 90 | 91 | w interface { 92 | io.Writer 93 | io.ByteWriter 94 | } 95 | } 96 | 97 | func (t *teeByteReader) ReadByte() (byte, error) { 98 | c, err := t.r.ReadByte() 99 | if err := t.w.WriteByte(c); err != nil { 100 | return c, err 101 | } 102 | return c, err 103 | } 104 | 105 | func (t *teeByteReader) Read(p []byte) (int, error) { 106 | n, err := t.r.Read(p) 107 | if n > 0 { 108 | if n, err := t.w.Write(p[:n]); err != nil { 109 | return n, err 110 | } 111 | } 112 | return n, err 113 | } 114 | -------------------------------------------------------------------------------- /pkg/apk/expandapk/split_test.go: -------------------------------------------------------------------------------- 1 | package expandapk 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestSplit(t *testing.T) { 12 | file := "testdata/hello-wolfi-2.12.1-r0.apk" 13 | 14 | f, err := os.Open(file) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer f.Close() 19 | 20 | parts, err := Split(f) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | if got, want := len(parts), 3; got != want { 26 | t.Fatalf("len(Split()): %d != %d", got, want) 27 | } 28 | 29 | f2, err := os.Open(file) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer f2.Close() 34 | 35 | exp, err := ExpandApk(context.Background(), f2, "") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | defer exp.Close() 40 | 41 | checks := []string{exp.SignatureFile, exp.ControlFile, exp.PackageFile} 42 | 43 | for i, part := range parts { 44 | check, err := os.Open(checks[i]) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | defer check.Close() 49 | 50 | want, err := io.ReadAll(check) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | got, err := io.ReadAll(part) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if !bytes.Equal(got, want) { 60 | t.Errorf("Split() != ExpandAPK() for part %d (%d, %d)", i, len(got), len(want)) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/apk/expandapk/testdata: -------------------------------------------------------------------------------- 1 | ../apk/testdata/ -------------------------------------------------------------------------------- /pkg/apk/expandapk/utility.go: -------------------------------------------------------------------------------- 1 | package expandapk 2 | 3 | import ( 4 | "archive/tar" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func checksumFromHeader(header *tar.Header) ([]byte, error) { 12 | pax := header.PAXRecords 13 | if pax == nil { 14 | return nil, nil 15 | } 16 | 17 | hexsum, ok := pax[paxRecordsChecksumKey] 18 | if !ok { 19 | return nil, nil 20 | } 21 | 22 | if strings.HasPrefix(hexsum, "Q1") { 23 | // This is nonstandard but something we did at one point, handle it. 24 | // In other contexts, this Q1 prefix means "this is sha1 not md5". 25 | b64 := strings.TrimPrefix(hexsum, "Q1") 26 | 27 | checksum, err := base64.StdEncoding.DecodeString(b64) 28 | if err != nil { 29 | return nil, fmt.Errorf("decoding base64 checksum from header for %q: %w", header.Name, err) 30 | } 31 | 32 | return checksum, nil 33 | } 34 | 35 | checksum, err := hex.DecodeString(hexsum) 36 | if err != nil { 37 | return nil, fmt.Errorf("decoding hex checksum from header for %q: %w", header.Name, err) 38 | } 39 | 40 | return checksum, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/apk/fs/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // package fs provides filesystem interface and implementations. 16 | // All implementations support fs.FS, but also supports full read-write, as well as all 17 | // filesystem features, even those not supported by other OSes, e.g. Chown 18 | // on Windows and Plan9, or Mknod on non-Unix-like. It even supports 19 | // those when running without appropriate permissions, e.g. Chown or 20 | // Mknod when non-root. 21 | // It is up to each implementation to determine how to handle requests for additional 22 | // capabilities, such as Chown when running as non-root. These can be special 23 | // files on disk, kept in-memory, or even coloured strips on the computer, as long as the 24 | // writes and reads are consistent. 25 | // All implementations are expected to be case-sensitive. 26 | 27 | package fs 28 | -------------------------------------------------------------------------------- /pkg/apk/fs/fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package fs 15 | 16 | import ( 17 | "io" 18 | "io/fs" 19 | "time" 20 | ) 21 | 22 | // FullFS is a filesystem that supports all filesystem operations. 23 | type FullFS interface { 24 | Mkdir(path string, perm fs.FileMode) error 25 | MkdirAll(path string, perm fs.FileMode) error 26 | Open(name string) (fs.File, error) 27 | OpenReaderAt(name string) (File, error) 28 | OpenFile(name string, flag int, perm fs.FileMode) (File, error) 29 | ReadFile(name string) ([]byte, error) 30 | WriteFile(name string, b []byte, mode fs.FileMode) error 31 | ReadDir(name string) ([]fs.DirEntry, error) 32 | Mknod(path string, mode uint32, dev int) error 33 | Readnod(name string) (dev int, err error) 34 | Symlink(oldname, newname string) error 35 | Link(oldname, newname string) error 36 | Readlink(name string) (target string, err error) 37 | Stat(path string) (fs.FileInfo, error) 38 | Lstat(path string) (fs.FileInfo, error) 39 | Create(name string) (File, error) 40 | Remove(name string) error 41 | Chmod(path string, perm fs.FileMode) error 42 | Chown(path string, uid int, gid int) error 43 | Chtimes(path string, atime time.Time, mtime time.Time) error 44 | SetXattr(path string, attr string, data []byte) error 45 | GetXattr(path string, attr string) ([]byte, error) 46 | RemoveXattr(path string, attr string) error 47 | ListXattrs(path string) (map[string][]byte, error) 48 | Sub(path string) (FullFS, error) 49 | } 50 | 51 | // File is an interface for a file. It includes Read, Write, Close. 52 | // This wouldn't be necessary if os.File were an interface, or if fs.File 53 | // were read/write. 54 | type File interface { 55 | fs.File 56 | io.WriteSeeker 57 | io.ReaderAt 58 | } 59 | 60 | type ReadLinkFS interface { 61 | fs.FS 62 | Readlink(name string) (string, error) 63 | } 64 | 65 | type OpenReaderAtFS interface { 66 | fs.FS 67 | OpenReaderAt(name string) (File, error) 68 | } 69 | 70 | type ReadnodFS interface { 71 | fs.FS 72 | Readnod(name string) (dev int, err error) 73 | } 74 | 75 | type OpenReaderAtReadLinkFS interface { 76 | OpenReaderAtFS 77 | ReadLinkFS 78 | } 79 | 80 | type OpenReaderAtReadLinkReadnodFS interface { 81 | OpenReaderAtFS 82 | ReadLinkFS 83 | ReadnodFS 84 | } 85 | 86 | type XattrFS interface { 87 | fs.FS 88 | SetXattr(path string, attr string, data []byte) error 89 | GetXattr(path string, attr string) ([]byte, error) 90 | RemoveXattr(path string, attr string) error 91 | ListXattrs(path string) (map[string][]byte, error) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/apk/fs/testdata/hello-2.12-r0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chainguard-dev/apko/deb6baee3472d68613f1e9df9a9af95d088fd6e5/pkg/apk/fs/testdata/hello-2.12-r0.apk -------------------------------------------------------------------------------- /pkg/apk/signature/rsa.go: -------------------------------------------------------------------------------- 1 | // From: https://raw.githubusercontent.com/goreleaser/nfpm/main/internal/sign/rsa.go 2 | // SPDX-License-Identifier: MIT 3 | 4 | package signature 5 | 6 | import ( 7 | "crypto" 8 | "crypto/rand" 9 | "crypto/rsa" 10 | _ "crypto/sha1" //nolint:gosec 11 | _ "crypto/sha256" 12 | "crypto/x509" 13 | "encoding/pem" 14 | "errors" 15 | "fmt" 16 | "os" 17 | ) 18 | 19 | var ( 20 | errNoPemBlock = errors.New("no PEM block found") 21 | errDigestLength = errors.New("digest has unexpected length") 22 | errNoPassphrase = errors.New("key is encrypted but no passphrase was provided") 23 | errNoRSAKey = errors.New("key is not an RSA key") 24 | errWeakDigest = errors.New("creating sha1 signatures not supported") 25 | ) 26 | 27 | // RSASignDigest signs the provided message digest. The key file must 28 | // be in the PEM format and can either be encrypted or not. 29 | func RSASignDigest(digest []byte, digestType crypto.Hash, keyFile, passphrase string) ([]byte, error) { 30 | if digestType == crypto.SHA1 { 31 | return nil, errWeakDigest 32 | } 33 | if len(digest) != digestType.Size() { 34 | return nil, errDigestLength 35 | } 36 | 37 | keyFileContent, err := os.ReadFile(keyFile) 38 | if err != nil { 39 | return nil, fmt.Errorf("reading key file: %w", err) 40 | } 41 | 42 | block, _ := pem.Decode(keyFileContent) 43 | if block == nil { 44 | return nil, errNoPemBlock 45 | } 46 | 47 | blockData := block.Bytes 48 | if x509.IsEncryptedPEMBlock(block) { //nolint:staticcheck 49 | if passphrase == "" { 50 | return nil, errNoPassphrase 51 | } 52 | 53 | var decryptedBlockData []byte 54 | 55 | decryptedBlockData, err = x509.DecryptPEMBlock(block, []byte(passphrase)) //nolint:staticcheck 56 | if err != nil { 57 | return nil, fmt.Errorf("decrypt private key PEM block: %w", err) 58 | } 59 | 60 | blockData = decryptedBlockData 61 | } 62 | 63 | priv, err := x509.ParsePKCS1PrivateKey(blockData) 64 | if err != nil { 65 | return nil, fmt.Errorf("parse PKCS1 private key: %w", err) 66 | } 67 | 68 | signature, err := priv.Sign(rand.Reader, digest, digestType) 69 | if err != nil { 70 | return nil, fmt.Errorf("signing: %w", err) 71 | } 72 | 73 | return signature, nil 74 | } 75 | 76 | // RSAVerifyDigest is exported for use in tests and verifies a 77 | // signature over the provided hash of a message. The key file must be 78 | // in the PEM format. 79 | func RSAVerifyDigest(digest []byte, digestType crypto.Hash, signature []byte, publicKey []byte) error { 80 | if len(digest) != digestType.Size() { 81 | return errDigestLength 82 | } 83 | 84 | block, _ := pem.Decode(publicKey) 85 | if block == nil { 86 | return errNoPemBlock 87 | } 88 | 89 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 90 | if err != nil { 91 | return fmt.Errorf("parse PKIX public key: %w", err) 92 | } 93 | 94 | rsaPub, ok := pub.(*rsa.PublicKey) 95 | if !ok { 96 | return errNoRSAKey 97 | } 98 | 99 | err = rsa.VerifyPKCS1v15(rsaPub, digestType, digest, signature) 100 | if err != nil { 101 | return fmt.Errorf("verify PKCS1v15 signature: %w", err) 102 | } 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/build/accounts_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build 16 | 17 | import ( 18 | "testing" 19 | 20 | "chainguard.dev/apko/pkg/build/types" 21 | ) 22 | 23 | var ( 24 | id0 = uint32(0) 25 | id0T = types.GID(&id0) 26 | id1234 = uint32(1234) 27 | id1235 = uint32(1235) 28 | id1235T = types.GID(&id1235) 29 | ) 30 | 31 | func Test_userToUserEntry_UID_GID_mapping(t *testing.T) { 32 | for _, test := range []struct { 33 | desc string 34 | user types.User 35 | expectedUID uint32 36 | expectedGID uint32 37 | }{ 38 | { 39 | desc: "Unique GID gets propogated", 40 | user: types.User{ 41 | UID: id1234, 42 | GID: id1235T, 43 | }, 44 | expectedUID: id1234, 45 | expectedGID: id1235, 46 | }, 47 | { 48 | desc: "Nil GID defaults to UID", 49 | user: types.User{ 50 | UID: id1234, 51 | }, 52 | expectedUID: id1234, 53 | expectedGID: id1234, 54 | }, 55 | { 56 | desc: "Able to set GID to 0", 57 | user: types.User{ 58 | UID: id1234, 59 | GID: id0T, 60 | }, 61 | expectedUID: id1234, 62 | expectedGID: id0, 63 | }, 64 | { 65 | // TODO: This may be unintentional but matches historical behavior 66 | desc: "Missing UID and GID means both are 0", 67 | user: types.User{}, 68 | expectedUID: id0, 69 | expectedGID: id0, 70 | }, 71 | } { 72 | userEntry := userToUserEntry(test.user) 73 | if userEntry.UID != test.expectedUID { 74 | t.Errorf("%s: expected UID %d got UID %d", test.desc, test.expectedUID, userEntry.UID) 75 | } 76 | if userEntry.GID != test.expectedGID { 77 | t.Errorf("%s: expected GID %d got GID %d", test.desc, test.expectedGID, userEntry.GID) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/build/busybox_gen.go: -------------------------------------------------------------------------------- 1 | //go:build busybox_versions 2 | 3 | package build 4 | 5 | //go:generate go run ./busybox_gen_versions.go build 1.32.1 busybox_versions.go 6 | -------------------------------------------------------------------------------- /pkg/build/busybox_test.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "chainguard.dev/apko/pkg/apk/apk" 10 | apkfs "chainguard.dev/apko/pkg/apk/fs" 11 | ) 12 | 13 | // Copyright 2023 Chainguard, Inc. 14 | // 15 | // Licensed under the Apache License, Version 2.0 (the "License"); 16 | // you may not use this file except in compliance with the License. 17 | // You may obtain a copy of the License at 18 | // 19 | // http://www.apache.org/licenses/LICENSE-2.0 20 | // 21 | // Unless required by applicable law or agreed to in writing, software 22 | // distributed under the License is distributed on an "AS IS" BASIS, 23 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | // See the License for the specific language governing permissions and 25 | // limitations under the License. 26 | 27 | func TestInstallBusyboxSymlinks(t *testing.T) { 28 | // these are links that definitely do *not* exist when using the standard files 29 | fakeLinks := []string{"/bin/foo", "/bin/bar"} 30 | trueLinks := []string{"/bin/ls", "/bin/grep"} 31 | pkg := &apk.Package{ 32 | Name: "busybox", 33 | Version: "1.36.0", // version that we know exists in busybox_versions.go 34 | } 35 | buildBusybox := func(fsys apkfs.FullFS, t *testing.T) { 36 | var err error 37 | err = fsys.MkdirAll("/bin", 0755) 38 | require.NoError(t, err) 39 | err = fsys.MkdirAll("/etc/busybox-paths.d", 0755) 40 | require.NoError(t, err) 41 | err = fsys.WriteFile("/bin/busybox", []byte("busybox"), 0755) 42 | require.NoError(t, err) 43 | err = fsys.MkdirAll("/usr/lib/apk/db", 0755) 44 | require.NoError(t, err) 45 | pkgLines := apk.PackageToInstalled(pkg) 46 | err = fsys.WriteFile("/usr/lib/apk/db/installed", []byte(strings.Join(pkgLines, "\n")+"\n\n"), 0755) 47 | require.NoError(t, err) 48 | } 49 | installed := []*apk.InstalledPackage{{ 50 | Package: *pkg, 51 | }} 52 | t.Run("with busybox-paths manifest", func(t *testing.T) { 53 | var err error 54 | fsys := apkfs.NewMemFS() 55 | buildBusybox(fsys, t) 56 | err = fsys.WriteFile("/etc/busybox-paths.d/busybox", []byte(strings.Join(fakeLinks, "\n")), 0755) 57 | require.NoError(t, err) 58 | err = installBusyboxLinks(fsys, installed) 59 | require.NoError(t, err) 60 | for _, link := range fakeLinks { 61 | _, err := fsys.Lstat(link) 62 | require.NoError(t, err) 63 | target, err := fsys.Readlink(link) 64 | require.NoError(t, err) 65 | require.Equal(t, "/bin/busybox", target) 66 | } 67 | for _, link := range trueLinks { 68 | _, err := fsys.Lstat(link) 69 | require.Error(t, err) 70 | } 71 | }) 72 | t.Run("without busybox-paths manifest", func(t *testing.T) { 73 | var err error 74 | fsys := apkfs.NewMemFS() 75 | buildBusybox(fsys, t) 76 | err = installBusyboxLinks(fsys, installed) 77 | require.NoError(t, err) 78 | for _, link := range fakeLinks { 79 | _, err := fsys.Lstat(link) 80 | require.Error(t, err, "those links should not exist") 81 | } 82 | for _, link := range trueLinks { 83 | _, err := fsys.Lstat(link) 84 | require.NoError(t, err) 85 | target, err := fsys.Readlink(link) 86 | require.NoError(t, err) 87 | require.Equal(t, "/bin/busybox", target) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/build/chardevices.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "golang.org/x/sys/unix" 8 | 9 | apkfs "chainguard.dev/apko/pkg/apk/fs" 10 | ) 11 | 12 | func installCharDevices(fsys apkfs.FullFS) error { 13 | devices := []struct { 14 | path string 15 | major uint32 16 | minor uint32 17 | }{ 18 | {"/dev/zero", 1, 5}, 19 | {"/dev/urandom", 1, 9}, 20 | {"/dev/null", 1, 3}, 21 | {"/dev/random", 1, 8}, 22 | {"/dev/console", 5, 1}, 23 | } 24 | for _, dev := range devices { 25 | if _, err := fsys.Stat(dev.path); err == nil { 26 | continue 27 | } 28 | dir := filepath.Dir(dev.path) 29 | if err := fsys.MkdirAll(dir, 0755); err != nil { 30 | return fmt.Errorf("creating directory %s: %w", dir, err) 31 | } 32 | if err := fsys.Mknod(dev.path, unix.S_IFCHR, int(unix.Mkdev(dev.major, dev.minor))); err != nil { 33 | return fmt.Errorf("creating character device %s: %w", dev.path, err) 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/build/installable_from_lock.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fmt" 5 | 6 | "chainguard.dev/apko/pkg/build/types" 7 | "chainguard.dev/apko/pkg/lock" 8 | 9 | "chainguard.dev/apko/pkg/apk/apk" 10 | ) 11 | 12 | type installablePackage struct { 13 | name string 14 | url string 15 | checksum string 16 | } 17 | 18 | func (p installablePackage) URL() string { return p.url } 19 | 20 | func (p installablePackage) PackageName() string { return p.name } 21 | 22 | func (p installablePackage) ChecksumString() string { return p.checksum } 23 | 24 | func installablePackagesForArch(l lock.Lock, arch types.Architecture) ([]apk.InstallablePackage, error) { 25 | pkgs := make([]apk.InstallablePackage, 0, len(l.Contents.Packages)) 26 | for _, p := range l.Contents.Packages { 27 | if p.Architecture != arch.ToAPK() { 28 | continue 29 | } 30 | if p.Checksum == "" { 31 | return nil, fmt.Errorf("locked package %s has missing checksum (please regenerate the lock file with Apko >=0.13)", p.Name) 32 | } 33 | pkgs = append(pkgs, installablePackage{name: p.Name, url: p.URL, checksum: p.Checksum}) 34 | } 35 | return pkgs, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/build/oci/consts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oci 16 | 17 | const ( 18 | LocalDomain = "apko.local" 19 | LocalRepo = "cache" 20 | ) 21 | -------------------------------------------------------------------------------- /pkg/build/oci/index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oci 16 | 17 | import "testing" 18 | 19 | func TestGenerateIndex(t *testing.T) { 20 | 21 | } 22 | 23 | func TestGenerateDockerIndex(t *testing.T) { 24 | 25 | } 26 | 27 | func TestBuildIndex(t *testing.T) { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /pkg/build/oci/publish_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package oci 16 | 17 | import "testing" 18 | 19 | func TestPublishImage(t *testing.T) { 20 | 21 | } 22 | 23 | func TestPublishImageFromLayer(t *testing.T) { 24 | 25 | } 26 | 27 | func TestPublishIndex(t *testing.T) { 28 | 29 | } 30 | 31 | func TestPublishTagFromIndex(t *testing.T) { 32 | 33 | } 34 | 35 | func TestPublishTagFromImage(t *testing.T) { 36 | 37 | } 38 | 39 | func TestPublishImagesFromIndex(t *testing.T) { 40 | 41 | } 42 | 43 | func TestCopy(t *testing.T) { 44 | 45 | } 46 | -------------------------------------------------------------------------------- /pkg/build/sbom_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | apkfs "chainguard.dev/apko/pkg/apk/fs" 25 | ) 26 | 27 | func TestReadReleaseData(t *testing.T) { 28 | osinfoData := `# This is a comment that should be ignored. 29 | ID=wolfi 30 | NAME="Wolfi" 31 | PRETTY_NAME="Wolfi" 32 | VERSION_ID="2022, 20230914" 33 | HOME_URL="https://wolfi.dev" 34 | ` 35 | fsys := apkfs.NewMemFS() 36 | require.NoError(t, fsys.MkdirAll(filepath.Dir("/etc/os-release"), os.FileMode(0o644))) 37 | require.NoError(t, fsys.WriteFile("/etc/os-release", []byte(osinfoData), os.FileMode(0o644))) 38 | // Non existent file, should err 39 | info, err := readReleaseData(fsys) 40 | require.NoError(t, err) 41 | require.Equal(t, "wolfi", info.ID, "id") 42 | require.Equal(t, "Wolfi", info.Name, "name") 43 | require.Equal(t, "2022, 20230914", info.VersionID, "version") 44 | require.Equal(t, "Wolfi", info.PrettyName, "pretty name") 45 | } 46 | 47 | func TestReadReleaseData_EmptyDefaults(t *testing.T) { 48 | fsys := apkfs.NewMemFS() 49 | info, err := readReleaseData(fsys) 50 | require.NoError(t, err) 51 | require.Equal(t, "unknown", info.ID) 52 | require.Equal(t, "apko-generated image", info.Name) 53 | require.Equal(t, "unknown", info.VersionID) 54 | require.Equal(t, "", info.PrettyName) 55 | } 56 | 57 | func TestBadReleaseData(t *testing.T) { 58 | osinfoData := `hello, world! this is not a valid os-release file 59 | ` 60 | fsys := apkfs.NewMemFS() 61 | require.NoError(t, fsys.MkdirAll(filepath.Dir("/etc/os-release"), os.FileMode(0o644))) 62 | require.NoError(t, fsys.WriteFile("/etc/os-release", []byte(osinfoData), os.FileMode(0o644))) 63 | // Bad data in file should err. 64 | _, err := readReleaseData(fsys) 65 | require.Error(t, err) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/build/tarball_test.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "context" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "chainguard.dev/apko/pkg/apk/fs" 12 | ) 13 | 14 | func TestWriteTar(t *testing.T) { 15 | var buf bytes.Buffer 16 | var ( 17 | m = fs.NewMemFS() 18 | dir = "a" 19 | file = "a/b" 20 | ) 21 | err := m.MkdirAll(dir, 0o755) 22 | require.NoError(t, err, "error creating dir %s", dir) 23 | err = m.WriteFile(file, []byte("hello world"), 0o644) 24 | require.NoError(t, err, "error creating file %s", file) 25 | 26 | // set xattrs, then see if the tar gets it 27 | err = m.SetXattr(dir, "user.dir", []byte("foo")) 28 | require.NoError(t, err, "error setting xattr on %s", dir) 29 | err = m.SetXattr(file, "user.file", []byte("bar")) 30 | require.NoError(t, err, "error setting xattr on %s", file) 31 | tw := tar.NewWriter(&buf) 32 | err = writeTar(context.Background(), tw, m) 33 | require.NoError(t, err, "error writing tar") 34 | err = tw.Close() 35 | require.NoError(t, err, "error closing tar writer") 36 | 37 | // now should be able to read the tar and check the xattrs 38 | tr := tar.NewReader(&buf) 39 | hdr, err := tr.Next() 40 | require.NoError(t, err, "error reading dir tar header") 41 | require.Equal(t, dir, hdr.Name, "tar dir header name mismatch") 42 | require.Equal(t, "foo", hdr.PAXRecords[xattrTarPAXRecordsPrefix+"user.dir"], "tar header for dir xattr mismatch") 43 | 44 | hdr, err = tr.Next() 45 | require.NoError(t, err, "error reading file tar header") 46 | require.Equal(t, file, hdr.Name, "tar file header name mismatch") 47 | require.Equal(t, "bar", hdr.PAXRecords[xattrTarPAXRecordsPrefix+"user.file"], "tar header for file xattr mismatch") 48 | } 49 | -------------------------------------------------------------------------------- /pkg/build/testdata: -------------------------------------------------------------------------------- 1 | ../../internal/cli/testdata -------------------------------------------------------------------------------- /pkg/build/types/testdata/overlay/base.apko.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | build_repositories: 3 | - "secret repository" 4 | repositories: 5 | - "repository" 6 | keyring: 7 | - "key" 8 | packages: 9 | - "package" -------------------------------------------------------------------------------- /pkg/build/types/testdata/overlay/overlay.apko.yaml: -------------------------------------------------------------------------------- 1 | include: testdata/overlay/base.apko.yaml -------------------------------------------------------------------------------- /pkg/build/types/testdata/overlay/overlay_with_package.apko.yaml: -------------------------------------------------------------------------------- 1 | include: testdata/overlay/overlay.apko.yaml 2 | 3 | contents: 4 | build_repositories: 5 | - "other_secret repository" 6 | packages: 7 | - "other_package" -------------------------------------------------------------------------------- /pkg/build/types/testdata/users.apko.yaml: -------------------------------------------------------------------------------- 1 | contents: 2 | repositories: 3 | - "repository" 4 | keyring: 5 | - "key" 6 | packages: 7 | - "package" 8 | 9 | accounts: 10 | users: 11 | - gid: 1 12 | uid: 2 13 | homedir: "/not/home" 14 | username: "something-else" 15 | - uid: 3 16 | username: "user" 17 | -------------------------------------------------------------------------------- /pkg/cpio/layer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cpio 16 | 17 | import ( 18 | "archive/tar" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | 23 | v1 "github.com/google/go-containerregistry/pkg/v1" 24 | "github.com/u-root/u-root/pkg/cpio" 25 | ) 26 | 27 | func FromLayer(layer v1.Layer, dest io.Writer) error { 28 | // Open the filesystem layer to walk through the file. 29 | u, err := layer.Uncompressed() 30 | if err != nil { 31 | return err 32 | } 33 | defer u.Close() 34 | tarReader := tar.NewReader(u) 35 | 36 | w := cpio.NewDedupWriter(cpio.Newc.Writer(dest)) 37 | 38 | // Iterate through the tar archive entries 39 | for { 40 | header, err := tarReader.Next() 41 | if err == io.EOF { 42 | break // End of archive 43 | } 44 | if err != nil { 45 | fmt.Println("Error reading tar entry:", err) 46 | return err 47 | } 48 | 49 | // Determine CPIO file mode based on TAR typeflag 50 | switch header.Typeflag { 51 | case tar.TypeDir: 52 | if err := cpio.WriteRecordsAndDirs(w, []cpio.Record{ 53 | cpio.Directory(header.Name, uint64(header.Mode)), 54 | }); err != nil { 55 | return err 56 | } 57 | 58 | case tar.TypeSymlink: 59 | if err := cpio.WriteRecordsAndDirs(w, []cpio.Record{ 60 | cpio.Symlink(header.Name, header.Linkname), 61 | }); err != nil { 62 | return err 63 | } 64 | 65 | case tar.TypeReg: 66 | var original bytes.Buffer 67 | // TODO(mattmoor): Do something better here, but unfortunately the 68 | // cpio stuff wants a seekable reader, so coming from a tar reader 69 | // I'm not sure how much leeway we have to do something better 70 | // than buffering. 71 | //nolint:gosec 72 | if _, err := io.Copy(&original, tarReader); err != nil { 73 | fmt.Println("Error reading file content:", err) 74 | return err 75 | } 76 | 77 | if err := cpio.WriteRecordsAndDirs(w, []cpio.Record{ 78 | cpio.StaticFile(header.Name, original.String(), uint64(header.Mode)), 79 | }); err != nil { 80 | return err 81 | } 82 | 83 | case tar.TypeChar: 84 | if err := cpio.WriteRecordsAndDirs(w, []cpio.Record{ 85 | cpio.CharDev(header.Name, uint64(header.Mode), uint64(header.Devmajor), uint64(header.Devminor)), 86 | }); err != nil { 87 | return err 88 | } 89 | 90 | default: 91 | fmt.Printf("Unsupported TAR typeflag: %c for %s\n", header.Typeflag, header.Name) 92 | continue // Skip unsupported types 93 | } 94 | } 95 | 96 | return w.WriteRecord(cpio.TrailerRecord) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/lock/lock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package lock 16 | 17 | import ( 18 | "testing" 19 | 20 | "chainguard.dev/apko/pkg/build/types" 21 | ) 22 | 23 | func TestArch2LockedPackages(t *testing.T) { 24 | l := Lock{ 25 | Contents: LockContents{ 26 | Packages: []LockPkg{{ 27 | Name: "avx", 28 | Version: "1.2.3", 29 | Architecture: "x86_64", 30 | }, { 31 | Name: "sve", 32 | Version: "2.3.4", 33 | Architecture: "aarch64", 34 | }}, 35 | }, 36 | } 37 | 38 | archs := []types.Architecture{types.Architecture("aarch64")} 39 | if got, want := len(l.Arch2LockedPackages(archs)), 1; got != want { 40 | t.Errorf("wanted %d arch, got %d", want, got) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/passwd/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package passwd implements simple functions to parse and manipulate 16 | // /etc/passwd and /etc/group files 17 | package passwd 18 | -------------------------------------------------------------------------------- /pkg/passwd/group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package passwd 16 | 17 | import ( 18 | "bytes" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/require" 22 | 23 | apkfs "chainguard.dev/apko/pkg/apk/fs" 24 | ) 25 | 26 | func TestGroupParser(t *testing.T) { 27 | fsys := apkfs.DirFS("testdata") 28 | gf, err := ReadOrCreateGroupFile(fsys, "group") 29 | require.NoError(t, err) 30 | 31 | for _, ge := range gf.Entries { 32 | if ge.GID == 0 { 33 | require.Equal(t, "root", ge.GroupName, "gid 0 is not root") 34 | } 35 | 36 | if ge.GID == 65534 { 37 | require.Equal(t, "nobody", ge.GroupName, "gid 65534 is not nobody") 38 | } 39 | } 40 | } 41 | 42 | func TestGroupWriter(t *testing.T) { 43 | fsys := apkfs.DirFS("testdata") 44 | gf, err := ReadOrCreateGroupFile(fsys, "group") 45 | require.NoError(t, err) 46 | 47 | w := &bytes.Buffer{} 48 | require.NoError(t, gf.Write(w)) 49 | 50 | r := bytes.NewReader(w.Bytes()) 51 | gf2 := &GroupFile{} 52 | require.NoError(t, gf2.Load(r)) 53 | 54 | w2 := &bytes.Buffer{} 55 | require.NoError(t, gf2.Write(w2)) 56 | 57 | require.Equal(t, w.Bytes(), w2.Bytes()) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/passwd/passwd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package passwd 16 | 17 | import ( 18 | "bytes" 19 | "os" 20 | "testing" 21 | 22 | "github.com/stretchr/testify/require" 23 | 24 | apkfs "chainguard.dev/apko/pkg/apk/fs" 25 | ) 26 | 27 | func TestParser(t *testing.T) { 28 | fsys := apkfs.NewMemFS() 29 | passwd, err := os.ReadFile("testdata/passwd") 30 | require.NoError(t, err) 31 | err = fsys.MkdirAll("etc", 0o755) 32 | require.NoError(t, err) 33 | err = fsys.WriteFile("etc/passwd", passwd, 0o600) 34 | require.NoError(t, err) 35 | uf, err := ReadOrCreateUserFile(fsys, "etc/passwd") 36 | require.NoError(t, err) 37 | 38 | for _, ue := range uf.Entries { 39 | if ue.UID == 0 { 40 | require.Equal(t, "root", ue.UserName, "uid 0 is not root") 41 | } 42 | 43 | if ue.UID == 65534 { 44 | require.Equal(t, "nobody", ue.UserName, "uid 65534 is not nobody") 45 | } 46 | } 47 | } 48 | 49 | func TestWriter(t *testing.T) { 50 | fsys := apkfs.NewMemFS() 51 | passwd, err := os.ReadFile("testdata/passwd") 52 | require.NoError(t, err) 53 | err = fsys.MkdirAll("etc", 0o755) 54 | require.NoError(t, err) 55 | err = fsys.WriteFile("etc/passwd", passwd, 0o600) 56 | require.NoError(t, err) 57 | uf, err := ReadOrCreateUserFile(fsys, "etc/passwd") 58 | require.NoError(t, err) 59 | 60 | w := &bytes.Buffer{} 61 | require.NoError(t, uf.Write(w)) 62 | 63 | r := bytes.NewReader(w.Bytes()) 64 | uf2 := &UserFile{} 65 | require.NoError(t, uf2.Load(r)) 66 | 67 | w2 := &bytes.Buffer{} 68 | require.NoError(t, uf2.Write(w2)) 69 | 70 | require.Equal(t, w.Bytes(), w2.Bytes()) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/passwd/testdata/group: -------------------------------------------------------------------------------- 1 | root:x:0:root 2 | bin:x:1:root,bin,daemon 3 | daemon:x:2:root,bin,daemon 4 | sys:x:3:root,bin,adm 5 | adm:x:4:root,adm,daemon 6 | tty:x:5: 7 | disk:x:6:root,adm 8 | lp:x:7:lp 9 | mem:x:8: 10 | kmem:x:9: 11 | wheel:x:10:root 12 | floppy:x:11:root 13 | mail:x:12:mail 14 | news:x:13:news 15 | uucp:x:14:uucp 16 | man:x:15:man 17 | cron:x:16:cron 18 | console:x:17: 19 | audio:x:18: 20 | cdrom:x:19: 21 | dialout:x:20:root 22 | ftp:x:21: 23 | sshd:x:22: 24 | at:x:25:at 25 | tape:x:26:root 26 | video:x:27:root 27 | netdev:x:28: 28 | readproc:x:30: 29 | squid:x:31:squid 30 | gdm:x:32:gdm 31 | xfs:x:33:xfs 32 | kvm:x:34:kvm 33 | games:x:35: 34 | named:x:40:named 35 | mysql:x:60: 36 | postgres:x:70: 37 | cdrw:x:80: 38 | apache:x:81: 39 | nut:x:84: 40 | usb:x:85: 41 | avahi:x:86: 42 | vpopmail:x:89: 43 | users:x:100:games 44 | ntp:x:123: 45 | nofiles:x:200: 46 | qmail:x:201: 47 | postfix:x:207: 48 | postdrop:x:208: 49 | smmsp:x:209:smmsp 50 | slocate:x:245: 51 | abuild:x:300: 52 | utmp:x:406: 53 | nogroup:x:65533: 54 | nobody:x:65534: 55 | -------------------------------------------------------------------------------- /pkg/passwd/testdata/passwd: -------------------------------------------------------------------------------- 1 | root:x:0:0:root:/root:/bin/ash 2 | bin:x:1:1:bin:/bin:/bin/false 3 | daemon:x:2:2:daemon:/sbin:/bin/false 4 | adm:x:3:4:adm:/var/adm:/bin/false 5 | lp:x:4:7:lp:/var/spool/lpd:/bin/false 6 | sync:x:5:0:sync:/sbin:/bin/sync 7 | shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown 8 | halt:x:7:0:halt:/sbin:/sbin/halt 9 | mail:x:8:12:mail:/var/spool/mail:/bin/false 10 | news:x:9:13:news:/usr/lib/news:/bin/false 11 | uucp:x:10:14:uucp:/var/spool/uucppublic:/bin/false 12 | operator:x:11:0:operator:/root:/bin/sh 13 | man:x:13:15:man:/usr/man:/bin/false 14 | postmaster:x:14:12:postmaster:/var/spool/mail:/bin/false 15 | cron:x:16:16:cron:/var/spool/cron:/bin/false 16 | ftp:x:21:21::/var/lib/ftp:/bin/false 17 | sshd:x:22:22:sshd:/dev/null:/bin/false 18 | at:x:25:25:at:/var/spool/cron/atjobs:/bin/false 19 | squid:x:31:31:Squid:/var/cache/squid:/bin/false 20 | gdm:x:32:32:GDM:/var/lib/gdm:/bin/false 21 | xfs:x:33:33:X Font Server:/etc/X11/fs:/bin/false 22 | games:x:35:35:games:/usr/games:/bin/false 23 | named:x:40:40:bind:/var/bind:/bin/false 24 | mysql:x:60:60:mysql:/var/lib/mysql:/bin/false 25 | postgres:x:70:70::/var/lib/postgresql:/bin/sh 26 | apache:x:81:81:apache:/var/www:/bin/false 27 | nut:x:84:84:nut:/var/state/nut:/bin/false 28 | cyrus:x:85:12::/usr/cyrus:/bin/false 29 | vpopmail:x:89:89::/var/vpopmail:/bin/false 30 | ntp:x:123:123:NTP:/var/empty:/bin/false 31 | alias:x:200:200::/var/qmail/alias:/bin/false 32 | qmaild:x:201:200::/var/qmail:/bin/false 33 | qmaill:x:202:200::/var/qmail:/bin/false 34 | qmailp:x:203:200::/var/qmail:/bin/false 35 | qmailq:x:204:201::/var/qmail:/bin/false 36 | qmailr:x:205:201::/var/qmail:/bin/false 37 | qmails:x:206:201::/var/qmail:/bin/false 38 | postfix:x:207:207:postfix:/var/spool/postfix:/bin/false 39 | smmsp:x:209:209:smmsp:/var/spool/mqueue:/bin/false 40 | guest:x:405:100:guest:/dev/null:/dev/null 41 | nobody:x:65534:65534:nobody:/:/bin/false 42 | distcc:x:240:2:distccd:/dev/null:/bin/false 43 | -------------------------------------------------------------------------------- /pkg/paths/paths.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package paths 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "os" 20 | "path" 21 | "path/filepath" 22 | ) 23 | 24 | func ResolvePath(p string, includePaths []string) (string, error) { 25 | _, err := os.Stat(p) 26 | if err == nil { 27 | return p, nil 28 | } 29 | for _, pathPrefix := range includePaths { 30 | resolvedPath := path.Join(pathPrefix, p) 31 | _, err := os.Stat(resolvedPath) 32 | if err == nil { 33 | return resolvedPath, nil 34 | } 35 | } 36 | return "", os.ErrNotExist 37 | } 38 | 39 | // AdvertisedCachedFile will create a symlink at `dst` pointing to `src`. 40 | // 41 | // In the case that `dst` already exists, another process had already created the symlink 42 | // and we can safely move on. We will also perform a best-effort attempt to clean up the 43 | // unadvertised file at `src`. 44 | func AdvertiseCachedFile(src, dst string) error { 45 | // Prefer relative symlinks 46 | rel, err := filepath.Rel(filepath.Dir(dst), src) 47 | if err != nil { 48 | rel = src 49 | } 50 | // Check if the destination already exists. 51 | if _, err := os.Stat(dst); err == nil { 52 | // Since `src` is unadvertised, it is safe to remove it. Ideally we want this to succeeds, 53 | // but we don't want to fail a build just because we couldn't clean up. This will be 54 | // left for background clean up process based on age. 55 | _ = os.Remove(src) 56 | return nil 57 | } 58 | // Create the symlink. 59 | if err := os.Symlink(rel, dst); err != nil { 60 | // Ignore already exists errors. We don't even want to do clean up here even when 61 | // the symlink is pointing somewhere elese, to avoid relying too much on file system 62 | // remantics/eventual consistency, etc. 63 | if errors.Is(err, os.ErrExist) { 64 | return nil 65 | } 66 | return fmt.Errorf("linking (cached) %s to %s: %w", rel, dst, err) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/paths/paths_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package paths 15 | 16 | import ( 17 | "os" 18 | "path/filepath" 19 | "testing" 20 | ) 21 | 22 | func TestAdvertiseCachedFile(t *testing.T) { 23 | tmpDir := t.TempDir() 24 | src1 := tmpDir + "/src1.tmp" 25 | src2 := tmpDir + "/src2.tmp" 26 | content := "content" 27 | if err := os.WriteFile(src1, []byte(content), 0644); err != nil { 28 | t.Fatal(err) 29 | } 30 | if err := os.WriteFile(src2, []byte(content), 0644); err != nil { 31 | t.Fatal(err) 32 | } 33 | dst := tmpDir + "/target" 34 | t.Run("dst does not exists", func(t *testing.T) { 35 | if err := AdvertiseCachedFile(src1, dst); err != nil { 36 | t.Fatal(err) 37 | } 38 | dstContent, err := os.ReadFile(dst) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if string(dstContent) != content { 43 | t.Fatalf("content mismatch: %s != %s", string(dstContent), content) 44 | } 45 | }) 46 | 47 | t.Run("dst exists", func(t *testing.T) { 48 | if err := AdvertiseCachedFile(src2, dst); err != nil { 49 | t.Fatal(err) 50 | } 51 | // check the symlink 52 | rel1, err := filepath.Rel(filepath.Dir(dst), src1) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if l, err := os.Readlink(dst); err != nil { 57 | t.Fatal(err) 58 | } else if l != rel1 { 59 | t.Fatalf("symlink should stay in tact: %s != %s", l, src2) 60 | } 61 | 62 | // check that src2 is removed 63 | if _, err := os.Stat(src2); !os.IsNotExist(err) { 64 | t.Fatalf("src2 should be removed: %v", err) 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/s6/s6.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s6 16 | 17 | import ( 18 | apkfs "chainguard.dev/apko/pkg/apk/fs" 19 | ) 20 | 21 | type Services map[string]string 22 | 23 | type Context struct { 24 | fs apkfs.FullFS 25 | } 26 | 27 | func New(fs apkfs.FullFS) *Context { 28 | return &Context{ 29 | fs: fs, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/s6/supervision_tree.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package s6 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "path/filepath" 21 | 22 | "github.com/chainguard-dev/clog" 23 | ) 24 | 25 | func (sc *Context) WriteSupervisionTree(ctx context.Context, services Services) error { 26 | log := clog.FromContext(ctx) 27 | log.Debug("generating supervision tree") 28 | 29 | // generate the leaves 30 | for service, svccmd := range services { 31 | svcdir := filepath.Join("sv", service) 32 | if err := sc.fs.MkdirAll(svcdir, 0777); err != nil { 33 | return fmt.Errorf("could not make supervision directory: %w", err) 34 | } 35 | 36 | if err := sc.fs.WriteFile(filepath.Join(svcdir, "run"), []byte(fmt.Sprintf("#!/bin/execlineb\n%s\n", svccmd)), 0755); err != nil { 37 | return fmt.Errorf("could not write runfile: %w", err) 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/sbom/generator/generator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package generator 16 | 17 | import ( 18 | apkfs "chainguard.dev/apko/pkg/apk/fs" 19 | 20 | "chainguard.dev/apko/pkg/sbom/generator/spdx" 21 | "chainguard.dev/apko/pkg/sbom/options" 22 | ) 23 | 24 | type Generator interface { 25 | Key() string 26 | Ext() string 27 | Generate(*options.Options, string) error 28 | GenerateIndex(*options.Options, string) error 29 | } 30 | 31 | func Generators(fsys apkfs.FullFS) map[string]Generator { 32 | generators := map[string]Generator{} 33 | 34 | sx := spdx.New(fsys) 35 | generators[sx.Key()] = &sx 36 | 37 | return generators 38 | } 39 | -------------------------------------------------------------------------------- /pkg/sbom/generator/spdx/testdata/apk_sboms/_generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euo pipefail 4 | 5 | # Define an array of package name-version strings 6 | packages=( 7 | "font-ubuntu-0.869-r1" 8 | "libattr1-2.5.1-r2" 9 | "logstash-8-8.15.3-r4" 10 | "logstash-8-compat-8.15.3-r4" 11 | "unbound-1.23.0-r0" 12 | "unbound-libs-1.23.0-r0" 13 | "unbound-config-1.23.0-r0" 14 | ) 15 | 16 | # Base URL for downloading APKs 17 | base_url="https://packages.wolfi.dev/os/x86_64" 18 | 19 | # Loop through the array and process each package 20 | for pkg in "${packages[@]}"; do 21 | url="${base_url}/${pkg}.apk" 22 | output_path="${pkg}.spdx.json" 23 | curl -q "$url" | tar Ozx "var/lib/db/sbom/${pkg}.spdx.json" >"$output_path" 2>/dev/null 24 | done 25 | -------------------------------------------------------------------------------- /pkg/sbom/generator/spdx/testdata/apk_sboms/libattr1-2.5.1-r2.spdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "SPDXID": "SPDXRef-DOCUMENT-apk-libattr1-2.5.1-r2", 3 | "name": "apk-libattr1-2.5.1-r2", 4 | "spdxVersion": "SPDX-2.3", 5 | "creationInfo": { 6 | "created": "2023-01-28T06:47:56Z", 7 | "creators": [ 8 | "Tool: melange (v0.2.0-97-g0d91d11)", 9 | "Organization: Chainguard, Inc" 10 | ], 11 | "licenseListVersion": "3.18" 12 | }, 13 | "dataLicense": "CC0-1.0", 14 | "documentNamespace": "https://spdx.org/spdxdocs/chainguard/melange/", 15 | "documentDescribes": [ 16 | "SPDXRef-Package-libattr1-2.5.1-r2" 17 | ], 18 | "files": [ 19 | { 20 | "SPDXID": "SPDXRef-File--lib-libattr.so.1.1.2501", 21 | "fileName": "/lib/libattr.so.1.1.2501", 22 | "licenseConcluded": "NOASSERTION", 23 | "checksums": [ 24 | { 25 | "algorithm": "SHA1", 26 | "checksumValue": "1b3a09852617e25522cfb46410c7f68c4149a7b5" 27 | }, 28 | { 29 | "algorithm": "SHA256", 30 | "checksumValue": "575c60ac3c5a5201ef30cec6b8f6aded46c76a35b27eaf0700a617f95236c3cd" 31 | }, 32 | { 33 | "algorithm": "SHA512", 34 | "checksumValue": "b437a3dd87c7777b0c0ee0ecb7ce3d24794e027471891f9861ec08e6b30e2896e5962a887cda2170156721d99685405fea39cc655a8606e441fdf6c1b3697980" 35 | } 36 | ] 37 | } 38 | ], 39 | "packages": [ 40 | { 41 | "SPDXID": "SPDXRef-Package-libattr1-2.5.1-r2", 42 | "name": "libattr1", 43 | "versionInfo": "2.5.1-r2", 44 | "filesAnalyzed": true, 45 | "hasFiles": [ 46 | "SPDXRef-File--lib-libattr.so.1.1.2501" 47 | ], 48 | "licenseConcluded": "NOASSERTION", 49 | "licenseDeclared": "GPL-2.0-or-later", 50 | "downloadLocation": "NOASSERTION", 51 | "copyrightText": "TODO\n", 52 | "externalRefs": [ 53 | { 54 | "referenceCategory": "PACKAGE_MANAGER", 55 | "referenceLocator": "pkg:apk/wolfi/libattr1@2.5.1-r2?arch=x86_64", 56 | "referenceType": "purl" 57 | } 58 | ], 59 | "packageVerificationCode": { 60 | "packageVerificationCodeValue": "ac84254f783b469f1ea6212ab2645b7c839144f9" 61 | } 62 | } 63 | ], 64 | "relationships": [ 65 | { 66 | "spdxElementId": "SPDXRef-Package-libattr1-2.5.1-r2", 67 | "relationshipType": "CONTAINS", 68 | "relatedSpdxElement": "SPDXRef-File--lib-libattr.so.1.1.2501" 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-8.15.3-r4.spdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "SPDXID": "SPDXRef-DOCUMENT", 3 | "name": "apk-logstash-8-8.15.3-r4", 4 | "spdxVersion": "SPDX-2.3", 5 | "creationInfo": { 6 | "created": "2024-10-24T15:16:49Z", 7 | "creators": [ 8 | "Tool: melange (v0.14.7)", 9 | "Organization: Chainguard, Inc" 10 | ], 11 | "licenseListVersion": "3.22" 12 | }, 13 | "dataLicense": "CC0-1.0", 14 | "documentNamespace": "https://spdx.org/spdxdocs/chainguard/melange/0434ffabb9f0343f5dada78abdb2e419b2be48fb", 15 | "documentDescribes": [ 16 | "SPDXRef-Package-logstash-8-8.15.3-r4" 17 | ], 18 | "packages": [ 19 | { 20 | "SPDXID": "SPDXRef-Package-logstash-8-8.15.3-r4", 21 | "name": "logstash-8", 22 | "versionInfo": "8.15.3-r4", 23 | "filesAnalyzed": false, 24 | "licenseConcluded": "NOASSERTION", 25 | "licenseDeclared": "Apache-2.0", 26 | "downloadLocation": "NOASSERTION", 27 | "originator": "Organization: Wolfi", 28 | "supplier": "Organization: Wolfi", 29 | "copyrightText": "\n", 30 | "externalRefs": [ 31 | { 32 | "referenceCategory": "PACKAGE-MANAGER", 33 | "referenceLocator": "pkg:apk/wolfi/logstash-8@8.15.3-r4?arch=x86_64", 34 | "referenceType": "purl" 35 | } 36 | ] 37 | }, 38 | { 39 | "SPDXID": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504", 40 | "name": "logstash-8.yaml", 41 | "versionInfo": "c7d40faa38ddffa98700cfa4c2f9bde196acc504", 42 | "filesAnalyzed": false, 43 | "licenseConcluded": "NOASSERTION", 44 | "licenseDeclared": "Apache-2.0", 45 | "downloadLocation": "NOASSERTION", 46 | "originator": "Organization: Wolfi", 47 | "supplier": "Organization: Wolfi", 48 | "externalRefs": [ 49 | { 50 | "referenceCategory": "PACKAGE-MANAGER", 51 | "referenceLocator": "pkg:github/wolfi-dev/os@c7d40faa38ddffa98700cfa4c2f9bde196acc504#logstash-8.yaml", 52 | "referenceType": "purl" 53 | } 54 | ] 55 | }, 56 | { 57 | "SPDXID": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0", 58 | "name": "logstash", 59 | "versionInfo": "v8.15.3", 60 | "filesAnalyzed": false, 61 | "licenseConcluded": "NOASSERTION", 62 | "licenseDeclared": "Apache-2.0", 63 | "downloadLocation": "NOASSERTION", 64 | "originator": "Organization: Elastic", 65 | "supplier": "Organization: Elastic", 66 | "externalRefs": [ 67 | { 68 | "referenceCategory": "PACKAGE-MANAGER", 69 | "referenceLocator": "pkg:github/elastic/logstash@v8.15.3", 70 | "referenceType": "purl" 71 | } 72 | ] 73 | } 74 | ], 75 | "relationships": [ 76 | { 77 | "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4", 78 | "relationshipType": "DESCRIBED_BY", 79 | "relatedSpdxElement": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504" 80 | }, 81 | { 82 | "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4", 83 | "relationshipType": "GENERATED_FROM", 84 | "relatedSpdxElement": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0" 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /pkg/sbom/generator/spdx/testdata/expected_image_sboms/no-supplier.spdx.json: -------------------------------------------------------------------------------- 1 | { 2 | "SPDXID": "SPDXRef-DOCUMENT", 3 | "name": "sbom", 4 | "spdxVersion": "SPDX-2.3", 5 | "creationInfo": { 6 | "created": "0001-01-01T00:00:00Z", 7 | "creators": [ 8 | "Tool: apko (devel)", 9 | "Organization: Chainguard, Inc" 10 | ], 11 | "licenseListVersion": "3.16" 12 | }, 13 | "dataLicense": "CC0-1.0", 14 | "documentNamespace": "https://spdx.org/spdxdocs/apko/", 15 | "documentDescribes": [ 16 | "SPDXRef-Package-" 17 | ], 18 | "packages": [ 19 | { 20 | "SPDXID": "SPDXRef-Package-", 21 | "name": "", 22 | "versionInfo": "3.0", 23 | "filesAnalyzed": false, 24 | "description": "apko operating system layer", 25 | "downloadLocation": "NOASSERTION", 26 | "supplier": "Organization: Apko Images, Plc", 27 | "externalRefs": [ 28 | { 29 | "referenceCategory": "PACKAGE-MANAGER", 30 | "referenceLocator": "pkg:oci/image?mediaType=\u0026os=linux", 31 | "referenceType": "purl" 32 | } 33 | ] 34 | }, 35 | { 36 | "SPDXID": "SPDXRef-OperatingSystem-apko-images", 37 | "name": "apko-images", 38 | "versionInfo": "3.0", 39 | "filesAnalyzed": false, 40 | "description": "Operating System", 41 | "downloadLocation": "NOASSERTION", 42 | "supplier": "Organization: Apko Images, Plc", 43 | "primaryPackagePurpose": "OPERATING-SYSTEM" 44 | }, 45 | { 46 | "SPDXID": "SPDXRef-Package-libattr1-2.5.1-r2", 47 | "name": "libattr1", 48 | "versionInfo": "2.5.1-r2", 49 | "filesAnalyzed": false, 50 | "licenseConcluded": "NOASSERTION", 51 | "licenseDeclared": "GPL-2.0-or-later", 52 | "downloadLocation": "NOASSERTION", 53 | "originator": "Organization: Apko Images, Plc", 54 | "supplier": "Organization: Apko Images, Plc", 55 | "copyrightText": "TODO\n", 56 | "externalRefs": [ 57 | { 58 | "referenceCategory": "PACKAGE_MANAGER", 59 | "referenceLocator": "pkg:apk/wolfi/libattr1@2.5.1-r2?arch=x86_64", 60 | "referenceType": "purl" 61 | } 62 | ] 63 | } 64 | ], 65 | "relationships": [] 66 | } 67 | -------------------------------------------------------------------------------- /pkg/sbom/options/options_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package options 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestPurlQualifierString(t *testing.T) { 24 | for _, tc := range []struct { 25 | q PurlQualifiers 26 | e string 27 | }{ 28 | { 29 | // Single value pair 30 | PurlQualifiers{ 31 | "mediaType": "application/vnd.oci.image.index.v1+json", 32 | }, 33 | "mediaType=application%2Fvnd.oci.image.index.v1%2Bjson", 34 | }, 35 | { 36 | // Mutiple value pairs 37 | PurlQualifiers{ 38 | "arch": "386", 39 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 40 | "os": "linux", 41 | }, 42 | "arch=386&mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson&os=linux", 43 | }, 44 | } { 45 | require.Equal(t, tc.e, tc.q.String()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/sbom/sbom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sbom 16 | 17 | import ( 18 | "chainguard.dev/apko/pkg/sbom/options" 19 | ) 20 | 21 | var DefaultOptions = options.Options{ 22 | OS: options.OSInfo{ 23 | Name: "Chainguard, Inc.", // This populates the supplier for index SBOMs. 24 | }, 25 | ImageInfo: options.ImageInfo{ 26 | Images: []options.ArchImageInfo{}, 27 | }, 28 | FileName: "sbom", 29 | Formats: []string{"spdx"}, 30 | } 31 | -------------------------------------------------------------------------------- /pkg/tarfs/testdata: -------------------------------------------------------------------------------- 1 | ../../internal/cli/testdata -------------------------------------------------------------------------------- /pkg/vfs/dirfs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package vfs 16 | 17 | import ( 18 | "io" 19 | "io/fs" 20 | "os" 21 | "path/filepath" 22 | ) 23 | 24 | type dirFS string 25 | 26 | // DirFS is a DirFS implementation that is suitable for use as a 27 | // BaseFS. 28 | func DirFS(path string) (BaseFS, error) { 29 | return dirFS(path), nil 30 | } 31 | 32 | func (dir dirFS) finalPath(path string) string { 33 | return filepath.Join(string(dir), path) 34 | } 35 | 36 | func (dir dirFS) Create(path string) (io.WriteCloser, error) { 37 | return os.Create(dir.finalPath(path)) 38 | } 39 | 40 | func (dir dirFS) Open(path string) (fs.File, error) { 41 | return os.Open(dir.finalPath(path)) 42 | } 43 | 44 | func (dir dirFS) ReadDir(path string) ([]fs.DirEntry, error) { 45 | return os.ReadDir(dir.finalPath(path)) 46 | } 47 | 48 | func (dir dirFS) ReadFile(path string) ([]byte, error) { 49 | return os.ReadFile(dir.finalPath(path)) 50 | } 51 | 52 | func (dir dirFS) Stat(path string) (fs.FileInfo, error) { 53 | return os.Stat(dir.finalPath(path)) 54 | } 55 | 56 | func (dir dirFS) Remove(path string) error { 57 | return os.Remove(dir.finalPath(path)) 58 | } 59 | 60 | func (dir dirFS) RemoveAll(path string) error { 61 | return os.RemoveAll(dir.finalPath(path)) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/vfs/dirfs_unit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022, 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package vfs 16 | 17 | import ( 18 | "io" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestDirFS(t *testing.T) { 26 | dir, err := DirFS("testdata") 27 | require.NoError(t, err) 28 | 29 | dentry, err := dir.ReadDir(".") 30 | require.NoError(t, err) 31 | 32 | assert.Equal(t, len(dentry), 1, "There should only be one directory entry") 33 | assert.Equal(t, dentry[0].Name(), "etc", "That directory entry should be named etc") 34 | 35 | dentry, err = dir.ReadDir("./etc") 36 | require.NoError(t, err) 37 | 38 | assert.Equal(t, len(dentry), 1, "etc/ should only have one child entry") 39 | assert.Equal(t, dentry[0].Name(), "motd", "That directory entry should be named motd") 40 | 41 | st, err := dir.Stat("./etc") 42 | require.NoError(t, err) 43 | 44 | assert.Equal(t, st.IsDir(), true, "etc/ is a directory") 45 | 46 | st, err = dir.Stat("./etc/motd") 47 | require.NoError(t, err) 48 | 49 | assert.Equal(t, st.IsDir(), false, "etc/motd is a normal file") 50 | 51 | inF, err := dir.Open("./etc/motd") 52 | require.NoError(t, err) 53 | defer inF.Close() 54 | 55 | data, err := io.ReadAll(inF) 56 | require.NoError(t, err) 57 | 58 | assert.Equal(t, data, []byte("Hello world\n"), "motd should return Hello world") 59 | 60 | otherdata, err := dir.ReadFile("./etc/motd") 61 | require.NoError(t, err) 62 | assert.Equal(t, data, otherdata, "dir.ReadFile behavior should match os.ReadFile") 63 | 64 | outF, err := dir.Create("./etc/motd2") 65 | require.NoError(t, err) 66 | defer outF.Close() 67 | defer dir.Remove("./etc/motd2") 68 | 69 | _, err = outF.Write(data) 70 | require.NoError(t, err) 71 | 72 | moredata, err := dir.ReadFile("./etc/motd2") 73 | require.NoError(t, err) 74 | assert.Equal(t, data, moredata, "motd2 should match motd") 75 | } 76 | -------------------------------------------------------------------------------- /pkg/vfs/fsmode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build !darwin && !linux 16 | // +build !darwin,!linux 17 | 18 | package vfs 19 | 20 | import ( 21 | "errors" 22 | "os" 23 | ) 24 | 25 | func fileModeToStatMode(mode os.FileMode) error { 26 | return errors.New("unsupported") 27 | } 28 | -------------------------------------------------------------------------------- /pkg/vfs/fsmode_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build darwin 16 | // +build darwin 17 | 18 | package vfs 19 | 20 | import ( 21 | "os" 22 | ) 23 | 24 | func fileModeToStatMode(mode os.FileMode) uint16 { 25 | return uint16(mode) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/vfs/fsmode_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Chainguard, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:build linux 16 | // +build linux 17 | 18 | package vfs 19 | 20 | import ( 21 | "os" 22 | ) 23 | 24 | func fileModeToStatMode(mode os.FileMode) uint32 { 25 | return uint32(mode) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/vfs/testdata/etc/motd: -------------------------------------------------------------------------------- 1 | Hello world 2 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # Apko Release Process 2 | 3 | ## Patch releases 4 | 5 | The most common type of release of Apko is a patch release. Generally we should aim to do these as often as necessary to release _backward compatible_ changes, especially to release updated dependencies to fix vulnerabilities. 6 | 7 | To cut a release: 8 | - go to https://github.com/chainguard-dev/apko/releases/new 9 | - click "Choose a tag" then "Find or create a new tag" 10 | - type a new patch version tag for the latest minor version 11 | - for example, if the latest version is `v0.11.5`, create a patch release `v0.11.6` 12 | - click "Create new tag: v0.X.Y on publish" 13 | - you can leave the release title empty 14 | - click "Generate release notes" 15 | - make any editorial changes to the release notes you think are relevant 16 | - make sure "Set as the latest release" is checked 17 | - click **"Publish release"** 18 | 19 | ### Monitor the release automation 20 | 21 | Once the tag is pushed, the [`Create Release` action](https://github.com/chainguard-dev/apko/actions/workflows/release.yaml) 22 | will attach the appropriate release artifacts and update release notes. 23 | 24 | At the time of this writing, the release job takes 20 to 30 minutes to execute. 25 | 26 | Make any editorial changes to the release notes you think are necessary. 27 | You may want to highlight certain changes or remove items that aren't interesting. 28 | 29 | Once the `Release` action has been completed successfully, find your release on 30 | the [releases page](https://github.com/chainguard-dev/apko/releases) 31 | 32 | ### Update dependents 33 | 34 | Apko is used as a library in [Melange](https://github.com/chainguard-dev/melange), [`wolfictl`](https://wolfi.dev/wolifctl), and [`terraform-provider-apko`](https://github.com/chainguard-dev/terraform-provider-apko), among others. 35 | 36 | When you cut a release of Apko, particularly when it's to update a vulnerable dependency, you'll probably also want to pick up those changes in Apko's dependents. 37 | 38 | ## Minor releases 39 | 40 | Occasionally there are large or breaking changes to Apko that we want to highlight with a new minor release. 41 | A minor release should be cut shortly after a breaking change is made, so that regular patch releases don't release breaking changes. 42 | 43 | The process for cutting a release is exactly the same as above, except that you should pick a new minor version. 44 | 45 | For example, if the latest version is `v0.11.5`, create a minor release `v0.12.0`. 46 | 47 | ### Release [`terraform-provider-apko`](https://github.com/chainguard-dev/terraform-provider-apko) 48 | 49 | For consistency, we prefer to keep minor releases of Apko in-line with minor releases of the Terraform provider. 50 | 51 | This means that when you cut `v0.12.0`, we should also cut a release of tf-apko to `v0.12.0`. 52 | 53 | We don't bother trying to keep patch releases in-sync between these repos, so you don't have to do this on every new patch release. 54 | --------------------------------------------------------------------------------