Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package apko for openSUSE:Factory checked in at 2026-03-11 20:54:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apko (Old) and /work/SRC/openSUSE:Factory/.apko.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "apko" Wed Mar 11 20:54:54 2026 rev:100 rq:1338202 version:1.1.14 Changes: -------- --- /work/SRC/openSUSE:Factory/apko/apko.changes 2026-03-09 16:15:32.423254213 +0100 +++ /work/SRC/openSUSE:Factory/.apko.new.8177/apko.changes 2026-03-11 20:56:44.181429289 +0100 @@ -1,0 +2,6 @@ +Wed Mar 11 05:59:40 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.1.14: + * Add support for custom certificate packages (#2105) + +------------------------------------------------------------------- Old: ---- apko-1.1.13.obscpio New: ---- apko-1.1.14.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apko.spec ++++++ --- /var/tmp/diff_new_pack.HlU56C/_old 2026-03-11 20:56:46.321517582 +0100 +++ /var/tmp/diff_new_pack.HlU56C/_new 2026-03-11 20:56:46.325517746 +0100 @@ -17,7 +17,7 @@ Name: apko -Version: 1.1.13 +Version: 1.1.14 Release: 0 Summary: Build OCI images from APK packages directly without Dockerfile License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.HlU56C/_old 2026-03-11 20:56:46.357519067 +0100 +++ /var/tmp/diff_new_pack.HlU56C/_new 2026-03-11 20:56:46.361519232 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/apko</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.1.13</param> + <param name="revision">v1.1.14</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.HlU56C/_old 2026-03-11 20:56:46.381520057 +0100 +++ /var/tmp/diff_new_pack.HlU56C/_new 2026-03-11 20:56:46.385520222 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/apko</param> - <param name="changesrevision">0a1df0f2e7cb29b95680746b8bbb0c03e5a22511</param></service></servicedata> + <param name="changesrevision">238cd7b8aec624af67bcd08492c7bb4412dc119d</param></service></servicedata> (No newline at EOF) ++++++ apko-1.1.13.obscpio -> apko-1.1.14.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/examples/package-certificates.yaml new/apko-1.1.14/examples/package-certificates.yaml --- old/apko-1.1.13/examples/package-certificates.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.1.14/examples/package-certificates.yaml 2026-03-10 19:28:46.000000000 +0100 @@ -0,0 +1,24 @@ +contents: + keyring: + - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub + - ./internal/cli/testdata/melange.rsa.pub + repositories: + - https://packages.wolfi.dev/os + - ./internal/cli/testdata/packages + packages: + - ca-certificates-bundle + - busybox + - apk-tools + - java-cacerts + - custom-ca-certs-1 + - custom-ca-certs-2 + +certificates: + providers: + - custom-ca-certificates + +entrypoint: + command: /bin/sh -c + +archs: + - amd64 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/hack/test-certificates.sh new/apko-1.1.14/hack/test-certificates.sh --- old/apko-1.1.13/hack/test-certificates.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.1.14/hack/test-certificates.sh 2026-03-10 19:28:46.000000000 +0100 @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +# Copyright 2025 Chainguard, Inc. +# SPDX-License-Identifier: Apache-2.0 + +# Tests that an apko-built image contains expected certificates in both the CA +# bundle and Java truststore, and that running update-ca-certificates doesn't +# change them. +# +# Usage: hack/test-certificates.sh <yaml> <fingerprint> [<fingerprint> ...] +# +# Example: +# hack/test-certificates.sh ./examples/certificates.yaml \ +# "E7:05:70:A9:..." "9B:2A:33:9F:..." + +set -euo pipefail + +if [ $# -lt 2 ]; then + echo "Usage: $0 <yaml> <fingerprint> [<fingerprint> ...]" + exit 1 +fi + +yaml="$1" +shift +fingerprints=("$@") + +name=$(basename "${yaml}" .yaml) +image="${name}:build" +image_arch="${name}:build-amd64" +tarball="/tmp/${name}.tar" + +# Gets all certificate fingerprints from a PEM stream, sorted, so two outputs can +# be semantically compared. +get_fingerprints() { + local cert="" + while IFS= read -r line; do + case "$line" in + "-----BEGIN CERTIFICATE-----") + cert="$line"$'\n' ;; + "-----END CERTIFICATE-----") + cert+="$line" + echo "$cert" | openssl x509 -noout -fingerprint -sha256 2>/dev/null + cert="" ;; + *) + [[ -n "$cert" ]] && cert+="$line"$'\n' || true ;; + esac + done | sort +} + +# Verifies that fingerprints file contains at least N certs and all expected fingerprints. +verify_fingerprints() { + local file="$1" + local min_count="$2" + local store_name="$3" + + local count + count=$(wc -l < "$file") + if [ "$count" -lt "$min_count" ]; then + echo "Expected at least $min_count certificates in $store_name, found $count" + exit 1 + fi + + for fp in "${fingerprints[@]}"; do + grep "$fp" "$file" + done + echo "$store_name contains all expected certificates." +} + +# Build the image. +make apko +./apko build "${yaml}" "${image}" "${tarball}" --arch amd64 +docker load < "${tarball}" + +# Get fingerprints from CA bundle and Java truststore before update-ca-certificates. +docker run --rm "${image_arch}" "cat /etc/ssl/certs/ca-certificates.crt" | get_fingerprints > /tmp/ca-bundle-fingerprints.txt +docker run --rm "${image_arch}" "trust extract --filter=ca-anchors --purpose=server-auth --format=pem-bundle /tmp/certs.pem && cat /tmp/certs.pem" | get_fingerprints > /tmp/java-truststore-fingerprints.txt + +# Verify both stores contain base certs and all expected certificates. +verify_fingerprints /tmp/ca-bundle-fingerprints.txt 10 "CA bundle" +verify_fingerprints /tmp/java-truststore-fingerprints.txt 10 "Java truststore" + +# Run update-ca-certificates and get fingerprints from both stores after. +docker run --rm "${image_arch}" "apk add ca-certificates && update-ca-certificates && cat /etc/ssl/certs/ca-certificates.crt" | get_fingerprints > /tmp/ca-bundle-updated-fingerprints.txt +docker run --rm "${image_arch}" "apk add ca-certificates && update-ca-certificates && trust extract --filter=ca-anchors --purpose=server-auth --format=pem-bundle /tmp/certs.pem && cat /tmp/certs.pem" | get_fingerprints > /tmp/java-truststore-updated-fingerprints.txt + +# Verify that the stores are semantically identical before and after update-ca-certificates. +diff /tmp/ca-bundle-fingerprints.txt /tmp/ca-bundle-updated-fingerprints.txt +echo "CA bundles before and after update-ca-certificates are identical." + +diff /tmp/java-truststore-fingerprints.txt /tmp/java-truststore-updated-fingerprints.txt +echo "Java truststores before and after update-ca-certificates are identical." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/hack/update-packages.sh new/apko-1.1.14/hack/update-packages.sh --- old/apko-1.1.13/hack/update-packages.sh 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/hack/update-packages.sh 2026-03-10 19:28:46.000000000 +0100 @@ -7,6 +7,8 @@ (cd internal/cli/testdata && \ 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 && \ - 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) + 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 && \ + 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 custom-ca-certs-1.melange.yaml && \ + 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 custom-ca-certs-2.melange.yaml) (cd internal/cli && apko lock ./testdata/apko.yaml) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/internal/cli/testdata/apko-certs.yaml new/apko-1.1.14/internal/cli/testdata/apko-certs.yaml --- old/apko-1.1.13/internal/cli/testdata/apko-certs.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.1.14/internal/cli/testdata/apko-certs.yaml 2026-03-10 19:28:46.000000000 +0100 @@ -0,0 +1,17 @@ +contents: + keyring: + - ./testdata/melange.rsa.pub + repositories: + - ./testdata/packages + packages: + - pretend-baselayout + - custom-ca-certs-1 + - custom-ca-certs-2 + +certificates: + providers: + - custom-ca-certificates + +archs: +- x86_64 +- aarch64 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/internal/cli/testdata/custom-ca-certs-1.melange.yaml new/apko-1.1.14/internal/cli/testdata/custom-ca-certs-1.melange.yaml --- old/apko-1.1.13/internal/cli/testdata/custom-ca-certs-1.melange.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.1.14/internal/cli/testdata/custom-ca-certs-1.melange.yaml 2026-03-10 19:28:46.000000000 +0100 @@ -0,0 +1,71 @@ +package: + name: custom-ca-certs-1 + version: 1.0.0 + epoch: 0 + description: "custom CA certificates package 1 (test)" + copyright: + - license: MIT + dependencies: + provides: + - custom-ca-certificates + +environment: + contents: + packages: + - busybox + +pipeline: + - name: Install certificates + runs: | + mkdir -p ${{targets.destdir}}/usr/local/share/ca-certificates + cat >${{targets.destdir}}/usr/local/share/ca-certificates/cert-a.crt <<'CERTEOF' + -----BEGIN CERTIFICATE----- + MIIFmDCCA4CgAwIBAgIQU9C87nMpOIFKYpfvOHFHFDANBgkqhkiG9w0BAQsFADBm + MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy + aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ + ZWFyIFgxMB4XDTE1MDYwNDExMDQzOFoXDTM1MDYwNDExMDQzOFowZjELMAkGA1UE + BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl + YXJjaCBHcm91cDEiMCAGA1UEAxMZKFNUQUdJTkcpIFByZXRlbmQgUGVhciBYMTCC + AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALbagEdDTa1QgGBWSYkyMhsc + ZXENOBaVRTMX1hceJENgsL0Ma49D3MilI4KS38mtkmdF6cPWnL++fgehT0FbRHZg + jOEr8UAN4jH6omjrbTD++VZneTsMVaGamQmDdFl5g1gYaigkkmx8OiCO68a4QXg4 + wSyn6iDipKP8utsE+x1E28SA75HOYqpdrk4HGxuULvlr03wZGTIf/oRt2/c+dYmD + oaJhge+GOrLAEQByO7+8+vzOwpNAPEx6LW+crEEZ7eBXih6VP19sTGy3yfqK5tPt + TdXXCOQMKAp+gCj/VByhmIr+0iNDC540gtvV303WpcbwnkkLYC0Ft2cYUyHtkstO + fRcRO+K2cZozoSwVPyB8/J9RpcRK3jgnX9lujfwA/pAbP0J2UPQFxmWFRQnFjaq6 + rkqbNEBgLy+kFL1NEsRbvFbKrRi5bYy2lNms2NJPZvdNQbT/2dBZKmJqxHkxCuOQ + FjhJQNeO+Njm1Z1iATS/3rts2yZlqXKsxQUzN6vNbD8KnXRMEeOXUYvbV4lqfCf8 + mS14WEbSiMy87GB5S9ucSV1XUrlTG5UGcMSZOBcEUpisRPEmQWUOTWIoDQ5FOia/ + GI+Ki523r2ruEmbmG37EBSBXdxIdndqrjy+QVAmCebyDx9eVEGOIpn26bW5LKeru + mJxa/CFBaKi4bRvmdJRLAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB + Af8EBTADAQH/MB0GA1UdDgQWBBS182Xy/rAKkh/7PH3zRKCsYyXDFDANBgkqhkiG + 9w0BAQsFAAOCAgEAncDZNytDbrrVe68UT6py1lfF2h6Tm2p8ro42i87WWyP2LK8Y + nLHC0hvNfWeWmjZQYBQfGC5c7aQRezak+tHLdmrNKHkn5kn+9E9LCjCaEsyIIn2j + qdHlAkepu/C3KnNtVx5tW07e5bvIjJScwkCDbP3akWQixPpRFAsnP+ULx7k0aO1x + qAeaAhQ2rgo1F58hcflgqKTXnpPM02intVfiVVkX5GXpJjK5EoQtLceyGOrkxlM/ + sTPq4UrnypmsqSagWV3HcUlYtDinc+nukFk6eR4XkzXBbwKajl0YjztfrCIHOn5Q + CJL6TERVDbM/aAPly8kJ1sWGLuvvWYzMYgLzDul//rUF10gEMWaXVZV51KpS9DY/ + 5CunuvCXmEQJHo7kGcViT7sETn6Jz9KOhvYcXkJ7po6d93A/jy4GKPIPnsKKNEmR + xUuXY4xRdh45tMJnLTUDdC9FIU0flTeO9/vNpVA8OPU1i14vCz+MU8KX1bV3GXm/ + fxlB7VBBjX9v5oUep0o/j68R/iDlCOM4VVfRa8gX6T2FU7fNdatvGro7uQzIvWof + gN9WUwCbEMBy/YhBSrXycKA8crgGg3x1mIsopn88JKwmMBa68oS7EHM9w7C4y71M + 7DiA+/9Qdp9RBWJpTS9i/mDnJg1xvo8Xz49mrrgfmcAXTCJqXi24NatI3Oc= + -----END CERTIFICATE----- + CERTEOF + cat >${{targets.destdir}}/usr/local/share/ca-certificates/cert-b.crt <<'CERTEOF' + -----BEGIN CERTIFICATE----- + MIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL + MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0 + eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj + b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE + BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl + YXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy + MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo + FQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT + VqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD + VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI + KoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo + uc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9 + vbtH7QiVzeKCOTQPINyRql6P + -----END CERTIFICATE----- + CERTEOF diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/internal/cli/testdata/custom-ca-certs-2.melange.yaml new/apko-1.1.14/internal/cli/testdata/custom-ca-certs-2.melange.yaml --- old/apko-1.1.13/internal/cli/testdata/custom-ca-certs-2.melange.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.1.14/internal/cli/testdata/custom-ca-certs-2.melange.yaml 2026-03-10 19:28:46.000000000 +0100 @@ -0,0 +1,48 @@ +package: + name: custom-ca-certs-2 + version: 1.0.0 + epoch: 0 + description: "custom CA certificates package 2 (test)" + copyright: + - license: MIT + dependencies: + provides: + - custom-ca-certificates + +environment: + contents: + packages: + - busybox + +pipeline: + - name: Install certificates + runs: | + mkdir -p ${{targets.destdir}}/usr/local/share/ca-certificates + cat >${{targets.destdir}}/usr/local/share/ca-certificates/cert-c.crt <<'CERTEOF' + -----BEGIN CERTIFICATE----- + MIIBwjCCAWegAwIBAgIUBKZDifzRAz30jwlcoQLIOxkBPLMwCgYIKoZIzj0EAwIw + NTEeMBwGA1UEAwwVVGVzdCBDQSBDZXJ0aWZpY2F0ZSAzMRMwEQYDVQQKDApUZXN0 + IE9yZyAzMCAXDTI2MDIyNzIwMzk1OVoYDzIxMjYwMjAzMjAzOTU5WjA1MR4wHAYD + VQQDDBVUZXN0IENBIENlcnRpZmljYXRlIDMxEzARBgNVBAoMClRlc3QgT3JnIDMw + WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARx/10O/q2rOnQtpBXHjARAUryfNWjD + UXeshzFk44hrv45loTsGQcyb5vAL6h3FSdBN91njUch4eF1NEYLKoR3Qo1MwUTAd + BgNVHQ4EFgQUhLbWEa0IUIixKPBVvuKxhK6UMnMwHwYDVR0jBBgwFoAUhLbWEa0I + UIixKPBVvuKxhK6UMnMwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBG + AiEAqgTlOPOiNJLPJhMjRl9Zpaq6TTGfh+awe7N3fcEdHVICIQDfgVRRkuv1KTWk + 44YBh2/IaTSFwFo8cd39Fnv7CYi/2g== + -----END CERTIFICATE----- + CERTEOF + cat >${{targets.destdir}}/usr/local/share/ca-certificates/cert-d.crt <<'CERTEOF' + -----BEGIN CERTIFICATE----- + MIIBwTCCAWegAwIBAgIUPrm4YvABD98JhdU93qPsAgryo0UwCgYIKoZIzj0EAwIw + NTEeMBwGA1UEAwwVVGVzdCBDQSBDZXJ0aWZpY2F0ZSA0MRMwEQYDVQQKDApUZXN0 + IE9yZyA0MCAXDTI2MDIyNzIwNDAwMFoYDzIxMjYwMjAzMjA0MDAwWjA1MR4wHAYD + VQQDDBVUZXN0IENBIENlcnRpZmljYXRlIDQxEzARBgNVBAoMClRlc3QgT3JnIDQw + WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbR9hBg7/IeSBYJzUvBUxnnaNmoOJj + ESG5CiOa2980CC5aixcLof5kk/9K16B+OLIGSUE+Ya98N0vNP8KmDmvBo1MwUTAd + BgNVHQ4EFgQU6ZlpZtkvodhxZX1aRsM44dY0SJ8wHwYDVR0jBBgwFoAU6ZlpZtkv + odhxZX1aRsM44dY0SJ8wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF + AiARCNSY4WZ7Tl1oAmWghJz0Sxzi57JY4pdrvzyzYQNrhgIhAPMAzTOf33fVRhaX + wB7TKj2HAGTDpoliTH80SMWJN3jK + -----END CERTIFICATE----- + CERTEOF Binary files old/apko-1.1.13/internal/cli/testdata/packages/aarch64/APKINDEX.tar.gz and new/apko-1.1.14/internal/cli/testdata/packages/aarch64/APKINDEX.tar.gz differ Binary files old/apko-1.1.13/internal/cli/testdata/packages/aarch64/custom-ca-certs-1-1.0.0-r0.apk and new/apko-1.1.14/internal/cli/testdata/packages/aarch64/custom-ca-certs-1-1.0.0-r0.apk differ Binary files old/apko-1.1.13/internal/cli/testdata/packages/aarch64/custom-ca-certs-2-1.0.0-r0.apk and new/apko-1.1.14/internal/cli/testdata/packages/aarch64/custom-ca-certs-2-1.0.0-r0.apk differ Binary files old/apko-1.1.13/internal/cli/testdata/packages/x86_64/APKINDEX.tar.gz and new/apko-1.1.14/internal/cli/testdata/packages/x86_64/APKINDEX.tar.gz differ Binary files old/apko-1.1.13/internal/cli/testdata/packages/x86_64/custom-ca-certs-1-1.0.0-r0.apk and new/apko-1.1.14/internal/cli/testdata/packages/x86_64/custom-ca-certs-1-1.0.0-r0.apk differ Binary files old/apko-1.1.13/internal/cli/testdata/packages/x86_64/custom-ca-certs-2-1.0.0-r0.apk and new/apko-1.1.14/internal/cli/testdata/packages/x86_64/custom-ca-certs-2-1.0.0-r0.apk differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/pkg/build/build_test.go new/apko-1.1.14/pkg/build/build_test.go --- old/apko-1.1.13/pkg/build/build_test.go 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/pkg/build/build_test.go 2026-03-10 19:28:46.000000000 +0100 @@ -124,6 +124,63 @@ require.Equal(t, installed[1].Version, "1.0.0-r0") } +func TestBuildImageWithCertPackages(t *testing.T) { + ctx := context.Background() + + opts := []build.Option{ + build.WithConfig("apko-certs.yaml", []string{"testdata"}), + } + + fsys := fs.NewMemFS() + + // Pre-create the CA bundle file (in a real image, the ca-certificates + // package provides this). installCertificates only appends to existing + // bundles. + require.NoError(t, fsys.MkdirAll("etc/ssl/certs", 0o755)) + require.NoError(t, fsys.WriteFile("etc/ssl/certs/ca-certificates.crt", []byte{}, 0o644)) + + bc, err := build.New(ctx, fsys, opts...) + if err != nil { + t.Fatal(err) + } + + if err := bc.BuildImage(ctx); err != nil { + t.Fatal(err) + } + + installed, err := bc.InstalledPackages() + if err != nil { + t.Fatal(err) + } + + // Should have pretend-baselayout + custom-ca-certs-1 + custom-ca-certs-2. + require.Len(t, installed, 3) + + // Verify the CA bundle contains all 4 certificates. + bundlePath := "etc/ssl/certs/ca-certificates.crt" + bundleData, err := fsys.ReadFile(bundlePath) + require.NoError(t, err, "CA bundle should exist at %s", bundlePath) + + bundle := string(bundleData) + require.Contains(t, bundle, "-----BEGIN CERTIFICATE-----") + + // Count the number of certificates in the bundle. + certCount := strings.Count(bundle, "-----BEGIN CERTIFICATE-----") + require.Equal(t, 4, certCount, "expected 4 certificates in the CA bundle, got %d", certCount) + + // Verify individual cert files exist in the filesystem (installed by packages). + certPaths := []string{ + "usr/local/share/ca-certificates/cert-a.crt", + "usr/local/share/ca-certificates/cert-b.crt", + "usr/local/share/ca-certificates/cert-c.crt", + "usr/local/share/ca-certificates/cert-d.crt", + } + for _, p := range certPaths { + _, err := fsys.Stat(p) + require.NoError(t, err, "cert file %s should exist", p) + } +} + func TestBuildImageFromLockFile(t *testing.T) { ctx := context.Background() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/pkg/build/certificates.go new/apko-1.1.14/pkg/build/certificates.go --- old/apko-1.1.13/pkg/build/certificates.go 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/pkg/build/certificates.go 2026-03-10 19:28:46.000000000 +0100 @@ -15,6 +15,7 @@ package build import ( + "archive/tar" "bytes" "context" "crypto/sha256" @@ -27,9 +28,14 @@ "io/fs" "os" "path/filepath" + "slices" + "sort" + "strings" "github.com/pavlo-v-chernykh/keystore-go/v4" "go.opentelemetry.io/otel" + + "chainguard.dev/apko/pkg/apk/apk" ) const ( @@ -67,28 +73,119 @@ ks keystore.KeyStore } -// installCertificates installs inline certificates into the build context. +// installCertificates installs certificates from two sources into the build context: +// 1. Inline certificates from the image configuration (bc.ic.Certificates.Additional). +// 2. Certificate files from installed packages that provide "custom-ca-certificates", +// replacing the role of update-ca-certificates post-install scripts. func (bc *Context) installCertificates(ctx context.Context) error { _, span := otel.Tracer("apko").Start(ctx, "installCertificates") defer span.End() - if bc.ic.Certificates == nil || len(bc.ic.Certificates.Additional) == 0 { - // No configuration, nothing to do. + if bc.ic.Certificates == nil { return nil } + // certToWrite pairs a parsed certificate with the metadata needed to write it. + type certToWrite struct { + cert *parsedCertificate + alias string // Java truststore alias + } + + certs := make([]certToWrite, 0, len(bc.ic.Certificates.Additional)) + builtTime, err := bc.GetBuildDateEpoch() if err != nil { return fmt.Errorf("failed to get build date epoch: %w", err) } - // Create the ca-certificates directory if it doesn't exist - if err := bc.fs.MkdirAll(caCertsDir, 0o755); err != nil { - return fmt.Errorf("failed to create ca-certificates directory: %w", err) + // Write inline certificates from the image configuration to individual files + // and collect them for downstream bundle/truststore appending. + if len(bc.ic.Certificates.Additional) > 0 { + // Create the ca-certificates directory + if err := bc.fs.MkdirAll(caCertsDir, 0o755); err != nil { + return fmt.Errorf("failed to create ca-certificates directory: %w", err) + } + for _, additional := range bc.ic.Certificates.Additional { + cert, err := parseCertificates(additional.Content) + if err != nil { + return fmt.Errorf("failed to parse certificate %s: %w", additional.Name, err) + } + // Write individual certificate file for update-ca-certificates to pick up. + // Name is validated not to do any path shenanigans on configuration validation. + // The fingerprint is controlled to be a hash and so also doesn't allow shenanigans. + certPath := filepath.Join(caCertsDir, fmt.Sprintf("%s-%s.crt", additional.Name, cert.fingerprint)) + if err := bc.fs.WriteFile(certPath, cert.pem, 0o644); err != nil { + return fmt.Errorf("failed to write certificate file %s: %w", certPath, err) + } + if err := bc.fs.Chtimes(certPath, builtTime, builtTime); err != nil { + return fmt.Errorf("failed to change times on certificate file %s: %w", certPath, err) + } + certs = append(certs, certToWrite{ + cert: cert, + alias: fmt.Sprintf("%s-%s", additional.Name, cert.fingerprint), + }) + } + } + + if len(bc.ic.Certificates.Providers) > 0 { + // Filter installed packages to find those that provide certificates + installed, err := bc.apk.GetInstalled() + if err != nil { + return fmt.Errorf("failed to get installed packages: %w", err) + } + var providerPkgs []*apk.InstalledPackage + for _, pkg := range installed { + for _, p := range pkg.Provides { + if slices.Contains(bc.ic.Certificates.Providers, p) { + providerPkgs = append(providerPkgs, pkg) + break + } + } + } + + // Collect certificate files from provider packages + var pkgCertFiles []string + for _, pkg := range providerPkgs { + for _, f := range pkg.Files { + // Directories are explicitly marked; regular files from ParseInstalled + // have Typeflag == 0 (not tar.TypeReg). + if f.Typeflag == tar.TypeDir { + continue + } + // Only consider pem/crt files under the caCertsDir + if !strings.HasPrefix(f.Name, caCertsDir+"/") { + continue + } + ext := filepath.Ext(f.Name) + if ext == ".crt" || ext == ".pem" { + pkgCertFiles = append(pkgCertFiles, f.Name) + } + } + } + // Sort for deterministic, reproducible builds. + sort.Strings(pkgCertFiles) + for _, certPath := range pkgCertFiles { + data, err := bc.fs.ReadFile(certPath) + if err != nil { + return fmt.Errorf("failed to read certificate file %s: %w", certPath, err) + } + cert, err := parseCertificates(string(data)) + if err != nil { + continue + } + certs = append(certs, certToWrite{ + cert: cert, + alias: fmt.Sprintf("pkg-%s", cert.fingerprint), + }) + } + } + + if len(certs) == 0 { + return nil } // Open handles for all existing CA bundles to append to. - existingBundles := make([]io.WriteSeeker, 0, len(caBundlePaths)) + existingBundles := make([]io.WriteCloser, 0, len(caBundlePaths)) for _, caBundlePath := range caBundlePaths { file, err := bc.fs.OpenFile(caBundlePath, os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { @@ -111,26 +208,11 @@ return fmt.Errorf("failed to load Java truststores: %w", err) } - for _, additional := range bc.ic.Certificates.Additional { - cert, err := parseCertificates(additional.Content) - if err != nil { - return fmt.Errorf("failed to parse certificate %s: %w", additional.Name, err) - } - - // Write individual certificate file for update-ca-certificates to pick up. - // Name is validated not to do any path shenanigans on configuration validation. - // The fingerprint is controlled to be a hash and so also doesn't allow shenanigans. - certPath := filepath.Join(caCertsDir, fmt.Sprintf("%s-%s.crt", additional.Name, cert.fingerprint)) - if err := bc.fs.WriteFile(certPath, cert.pem, 0o644); err != nil { - return fmt.Errorf("failed to write certificate file %s: %w", certPath, err) - } - if err := bc.fs.Chtimes(certPath, builtTime, builtTime); err != nil { - return fmt.Errorf("failed to change times on certificate file %s: %w", certPath, err) - } - + // Append all collected certificates to open CA bundles and Java truststores. + for _, c := range certs { // Append to all existing CA bundles. for _, bundle := range existingBundles { - if _, err := bundle.Write(cert.pem); err != nil { + if _, err := bundle.Write(c.cert.pem); err != nil { return fmt.Errorf("failed to append certificate to bundle: %w", err) } // Put newlines in-between certificates to mimic update-ca-certificates behavior. @@ -145,16 +227,16 @@ CreationTime: builtTime, Certificate: keystore.Certificate{ Type: "X.509", - Content: cert.structured.Raw, + Content: c.cert.structured.Raw, }, } - alias := fmt.Sprintf("%s-%s", additional.Name, cert.fingerprint) - if err := ts.ks.SetTrustedCertificateEntry(alias, entry); err != nil { + if err := ts.ks.SetTrustedCertificateEntry(c.alias, entry); err != nil { return fmt.Errorf("failed to add certificate to Java truststore: %w", err) } } } + // Update timestamps on all open CA bundle files. for _, caBundlePath := range caBundlePaths { if err := bc.fs.Chtimes(caBundlePath, builtTime, builtTime); err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("failed to change times on CA bundle %s: %w", caBundlePath, err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/pkg/build/certificates_test.go new/apko-1.1.14/pkg/build/certificates_test.go --- old/apko-1.1.13/pkg/build/certificates_test.go 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/pkg/build/certificates_test.go 2026-03-10 19:28:46.000000000 +0100 @@ -15,15 +15,19 @@ package build import ( + "archive/tar" "bytes" "context" "fmt" "io/fs" "path/filepath" + "strings" "testing" "time" + "chainguard.dev/apko/pkg/apk/apk" apkfs "chainguard.dev/apko/pkg/apk/fs" + apktypes "chainguard.dev/apko/pkg/apk/types" "chainguard.dev/apko/pkg/build/types" "chainguard.dev/apko/pkg/options" @@ -86,6 +90,38 @@ -----END CERTIFICATE----- ` testCertPEM2Fingerprint = "9b2a339fe6a3e85585c4cd75536cb8c1cf7cd603b9a64bec2521858ae48da85d" + + // Self-signed test certificate 3 (EC P-256, CN=Test CA Certificate 3). + testCertPEM3 = `-----BEGIN CERTIFICATE----- +MIIBwjCCAWegAwIBAgIUBKZDifzRAz30jwlcoQLIOxkBPLMwCgYIKoZIzj0EAwIw +NTEeMBwGA1UEAwwVVGVzdCBDQSBDZXJ0aWZpY2F0ZSAzMRMwEQYDVQQKDApUZXN0 +IE9yZyAzMCAXDTI2MDIyNzIwMzk1OVoYDzIxMjYwMjAzMjAzOTU5WjA1MR4wHAYD +VQQDDBVUZXN0IENBIENlcnRpZmljYXRlIDMxEzARBgNVBAoMClRlc3QgT3JnIDMw +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARx/10O/q2rOnQtpBXHjARAUryfNWjD +UXeshzFk44hrv45loTsGQcyb5vAL6h3FSdBN91njUch4eF1NEYLKoR3Qo1MwUTAd +BgNVHQ4EFgQUhLbWEa0IUIixKPBVvuKxhK6UMnMwHwYDVR0jBBgwFoAUhLbWEa0I +UIixKPBVvuKxhK6UMnMwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNJADBG +AiEAqgTlOPOiNJLPJhMjRl9Zpaq6TTGfh+awe7N3fcEdHVICIQDfgVRRkuv1KTWk +44YBh2/IaTSFwFo8cd39Fnv7CYi/2g== +-----END CERTIFICATE----- +` + testCertPEM3Fingerprint = "347537af7a09d403f19f58f83c3568912af24b7c12e745f1d5557079708c91ad" + + // Self-signed test certificate 4 (EC P-256, CN=Test CA Certificate 4). + testCertPEM4 = `-----BEGIN CERTIFICATE----- +MIIBwTCCAWegAwIBAgIUPrm4YvABD98JhdU93qPsAgryo0UwCgYIKoZIzj0EAwIw +NTEeMBwGA1UEAwwVVGVzdCBDQSBDZXJ0aWZpY2F0ZSA0MRMwEQYDVQQKDApUZXN0 +IE9yZyA0MCAXDTI2MDIyNzIwNDAwMFoYDzIxMjYwMjAzMjA0MDAwWjA1MR4wHAYD +VQQDDBVUZXN0IENBIENlcnRpZmljYXRlIDQxEzARBgNVBAoMClRlc3QgT3JnIDQw +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbR9hBg7/IeSBYJzUvBUxnnaNmoOJj +ESG5CiOa2980CC5aixcLof5kk/9K16B+OLIGSUE+Ya98N0vNP8KmDmvBo1MwUTAd +BgNVHQ4EFgQU6ZlpZtkvodhxZX1aRsM44dY0SJ8wHwYDVR0jBBgwFoAU6ZlpZtkv +odhxZX1aRsM44dY0SJ8wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF +AiARCNSY4WZ7Tl1oAmWghJz0Sxzi57JY4pdrvzyzYQNrhgIhAPMAzTOf33fVRhaX +wB7TKj2HAGTDpoliTH80SMWJN3jK +-----END CERTIFICATE----- +` + testCertPEM4Fingerprint = "12ae34999aa64dcd1a6947e838a53aababfcfaca45abca8dc0cbb8dcb7bd063c" ) func TestParseCertificates(t *testing.T) { @@ -170,13 +206,21 @@ return buf.Bytes() } + type pkgEntry struct { + pkg apktypes.Package + files []tar.Header + } + tests := []struct { name string - cfg *types.ImageCertificates + cfg *types.ImageCertificates // inline certs + pkgs []pkgEntry // package-provided certs + certData map[string][]byte // cert file contents for packages existingFiles map[string][]byte wantFiles map[string][]byte wantErr bool }{{ + // Inline certificate tests. name: "nil certificates config", cfg: nil, }, { @@ -255,7 +299,7 @@ filepath.Join(caCertsDir, fmt.Sprintf("test-cert-%s.crt", testCertPEMFingerprint)): []byte(testCertPEM), }, }, { - name: "certificate with existing Java truststore", + name: "inline certificate with existing Java truststore", cfg: &types.ImageCertificates{ Additional: []types.AdditionalCertificateEntry{ {Name: "test-cert", Content: testCertPEM}, @@ -276,7 +320,7 @@ }), }, }, { - name: "multiple certificates with existing Java truststore", + name: "multiple inline certificates with existing Java truststore", cfg: &types.ImageCertificates{ Additional: []types.AdditionalCertificateEntry{ {Name: "test-cert-1", Content: testCertPEM}, @@ -299,19 +343,223 @@ "test-cert-2-" + testCertPEM2Fingerprint: testCertPEM2, }), }, + }, { + // Package-provided certificate tests. + name: "no packages with custom-ca-certificates", + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "some-package", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"something-else"}, + }, + }}, + }, { + name: "package without custom-ca-certificates provide is ignored", + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "not-a-ca-pkg", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"something-else"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/sneaky-cert.crt", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/sneaky-cert.crt": []byte(testCertPEM), + }, + }, { + name: "single package with two certs appends to bundle", + cfg: &types.ImageCertificates{Providers: []string{"custom-ca-certificates"}}, + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "ca-certs-1", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert-a.crt", Mode: 0o644}, + {Name: "usr/local/share/ca-certificates/cert-b.crt", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/cert-a.crt": []byte(testCertPEM), + "usr/local/share/ca-certificates/cert-b.crt": []byte(testCertPEM2), + }, + existingFiles: map[string][]byte{ + caBundlePaths[0]: {}, + }, + wantFiles: map[string][]byte{ + caBundlePaths[0]: []byte(testCertPEM + "\n" + testCertPEM2 + "\n"), + }, + }, { + name: "two packages with certs each appends to existing bundle", + cfg: &types.ImageCertificates{Providers: []string{"custom-ca-certificates"}}, + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "ca-certs-1", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert-a.crt", Mode: 0o644}, + }, + }, { + pkg: apktypes.Package{ + Name: "ca-certs-2", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert-c.crt", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/cert-a.crt": []byte(testCertPEM3), + "usr/local/share/ca-certificates/cert-c.crt": []byte(testCertPEM4), + }, + existingFiles: map[string][]byte{ + caBundlePaths[0]: []byte("# Existing Bundle\n"), + }, + wantFiles: map[string][]byte{ + caBundlePaths[0]: []byte("# Existing Bundle\n" + testCertPEM3 + "\n" + testCertPEM4 + "\n"), + }, + }, { + name: "non-cert files in package are ignored", + cfg: &types.ImageCertificates{Providers: []string{"custom-ca-certificates"}}, + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "ca-certs-1", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert-a.crt", Mode: 0o644}, + {Name: "usr/local/share/ca-certificates/README.md", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/cert-a.crt": []byte(testCertPEM), + "usr/local/share/ca-certificates/README.md": []byte("not a cert"), + }, + existingFiles: map[string][]byte{ + caBundlePaths[0]: {}, + }, + wantFiles: map[string][]byte{ + caBundlePaths[0]: []byte(testCertPEM + "\n"), + }, + }, { + name: "package certs with existing Java truststore", + cfg: &types.ImageCertificates{Providers: []string{"custom-ca-certificates"}}, + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "ca-certs-1", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert-a.crt", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/cert-a.crt": []byte(testCertPEM), + }, + existingFiles: map[string][]byte{ + caBundlePaths[0]: {}, + javaTruststorePaths[0]: createTruststore(map[string]string{ + "existing": testCertPEM2, + }), + }, + wantFiles: map[string][]byte{ + caBundlePaths[0]: []byte(testCertPEM + "\n"), + javaTruststorePaths[0]: createTruststore(map[string]string{ + "existing": testCertPEM2, + "pkg-" + testCertPEMFingerprint: testCertPEM, + }), + }, + }, { + // Combined inline + package-provided certificate test. + name: "inline and package certs both appended to bundle and truststore", + cfg: &types.ImageCertificates{ + Additional: []types.AdditionalCertificateEntry{ + {Name: "inline-cert", Content: testCertPEM}, + }, + Providers: []string{"custom-ca-certificates"}, + }, + pkgs: []pkgEntry{{ + pkg: apktypes.Package{ + Name: "ca-certs-1", Version: "1.0.0", Arch: "x86_64", + Provides: []string{"custom-ca-certificates"}, + }, + files: []tar.Header{ + {Name: "usr", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates", Typeflag: tar.TypeDir, Mode: 0o755}, + {Name: "usr/local/share/ca-certificates/cert.crt", Mode: 0o644}, + }, + }}, + certData: map[string][]byte{ + "usr/local/share/ca-certificates/cert.crt": []byte(testCertPEM3), + }, + existingFiles: map[string][]byte{ + caBundlePaths[0]: []byte("# Existing Bundle\n"), + javaTruststorePaths[0]: createTruststore(map[string]string{ + "existing": testCertPEM2, + }), + }, + wantFiles: map[string][]byte{ + // Inline certs are processed first, then package certs. + caBundlePaths[0]: []byte("# Existing Bundle\n" + testCertPEM + "\n" + testCertPEM3 + "\n"), + filepath.Join(caCertsDir, fmt.Sprintf("inline-cert-%s.crt", testCertPEMFingerprint)): []byte(testCertPEM), + javaTruststorePaths[0]: createTruststore(map[string]string{ + "existing": testCertPEM2, + "inline-cert-" + testCertPEMFingerprint: testCertPEM, + "pkg-" + testCertPEM3Fingerprint: testCertPEM3, + }), + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fsys := apkfs.NewMemFS() - bc := &Context{ - o: options.Options{ - SourceDateEpoch: epoch, - }, - ic: types.ImageConfiguration{ - Certificates: tt.cfg, - }, - fs: fsys, + + apkInst, err := apk.New(context.Background(), apk.WithFS(fsys), apk.WithIgnoreMknodErrors(true)) + if err != nil { + t.Fatalf("failed to create APK: %v", err) + } + if err := apkInst.InitDB(context.Background()); err != nil { + t.Fatalf("failed to init APK DB: %v", err) + } + for _, p := range tt.pkgs { + if _, err := apkInst.AddInstalledPackage(&p.pkg, p.files); err != nil { + t.Fatalf("failed to add installed package %s: %v", p.pkg.Name, err) + } + } + for path, data := range tt.certData { + if err := fsys.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("failed to create dir for %s: %v", path, err) + } + if err := fsys.WriteFile(path, data, 0o644); err != nil { + t.Fatalf("failed to write cert file %s: %v", path, err) + } } for path, content := range tt.existingFiles { @@ -323,51 +571,77 @@ } } - err := bc.installCertificates(context.Background()) + bc := &Context{ + o: options.Options{ + SourceDateEpoch: epoch, + }, + ic: types.ImageConfiguration{ + Certificates: tt.cfg, + }, + fs: fsys, + apk: apkInst, + } + + err = bc.installCertificates(context.Background()) if (err != nil) != tt.wantErr { t.Fatalf("installCertificates() error = %v, wantErr %v", err, tt.wantErr) } if tt.wantErr { - // Expected error, nothing further to check - return - } - if tt.cfg == nil || len(tt.cfg.Additional) == 0 { - // Nothing further to check return } - // Walk the entire filesystem to ensure we're checking contents for all - // expected files. - fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - t.Fatalf("error walking to %s: %v", path, err) - } - if d.IsDir() { - return nil - } - - wantContent, ok := tt.wantFiles[path] - if !ok { - t.Errorf("unexpected file created: %s", path) - return nil + if len(tt.wantFiles) == 0 { + // No-op case: verify primary CA bundle was NOT created/modified. + if _, err := fsys.Stat(caBundlePaths[0]); err == nil { + t.Errorf("expected no CA bundle to be created, but %s exists", caBundlePaths[0]) } + return + } + // Verify expected file contents and timestamps. + for path, wantContent := range tt.wantFiles { data, err := fsys.ReadFile(path) if err != nil { t.Fatalf("failed to read expected file %s: %v", path, err) } - if diff := cmp.Diff(wantContent, data); diff != "" { t.Errorf("file content mismatch for %s (-want +got):\n%s", path, diff) } - stat, err := fsys.Stat(path) if err != nil { t.Fatalf("failed to stat file %s: %v", path, err) } - modTime := stat.ModTime() - if !modTime.Equal(epoch) { - t.Errorf("file %s has mod time %v, want %v", path, modTime, epoch) + if !stat.ModTime().Equal(epoch) { + t.Errorf("file %s has mod time %v, want %v", path, stat.ModTime(), epoch) + } + } + + // Build a set of files that exist on the filesystem but are + // not certificate output: APK DB files from InitDB and + // package cert source files written during test setup. + setupFiles := map[string]bool{} + if apkInst != nil { + for _, h := range apkInst.ListInitFiles() { + setupFiles[strings.TrimPrefix(h.Name, "/")] = true + } + } + for path := range tt.certData { + setupFiles[path] = true + } + + // Walk the entire filesystem to catch unexpected files. + fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + t.Fatalf("error walking to %s: %v", path, err) + } + if d.IsDir() { + return nil + } + if setupFiles[path] { + return nil + } + if _, ok := tt.wantFiles[path]; !ok { + t.Errorf("unexpected file created: %s", path) } return nil }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/pkg/build/types/schema.json new/apko-1.1.14/pkg/build/types/schema.json --- old/apko-1.1.13/pkg/build/types/schema.json 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/pkg/build/types/schema.json 2026-03-10 19:28:46.000000000 +0100 @@ -84,6 +84,13 @@ }, "type": "array", "description": "Additional certificates to install in the image" + }, + "providers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Providers is a list of virtual package names that identify packages\ncontaining CA certificate files to be assembled into the system CA bundle." } }, "additionalProperties": false, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.1.13/pkg/build/types/types.go new/apko-1.1.14/pkg/build/types/types.go --- old/apko-1.1.13/pkg/build/types/types.go 2026-03-08 16:29:25.000000000 +0100 +++ new/apko-1.1.14/pkg/build/types/types.go 2026-03-10 19:28:46.000000000 +0100 @@ -455,4 +455,7 @@ type ImageCertificates struct { // Additional certificates to install in the image Additional []AdditionalCertificateEntry `json:"additional,omitempty" yaml:"additional,omitempty"` + // Providers is a list of virtual package names that identify packages + // containing CA certificate files to be assembled into the system CA bundle. + Providers []string `json:"providers,omitempty" yaml:"providers,omitempty"` } ++++++ apko.obsinfo ++++++ --- /var/tmp/diff_new_pack.HlU56C/_old 2026-03-11 20:56:46.989545142 +0100 +++ /var/tmp/diff_new_pack.HlU56C/_new 2026-03-11 20:56:46.993545308 +0100 @@ -1,5 +1,5 @@ name: apko -version: 1.1.13 -mtime: 1772983765 -commit: 0a1df0f2e7cb29b95680746b8bbb0c03e5a22511 +version: 1.1.14 +mtime: 1773167326 +commit: 238cd7b8aec624af67bcd08492c7bb4412dc119d ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/apko/vendor.tar.gz /work/SRC/openSUSE:Factory/.apko.new.8177/vendor.tar.gz differ: char 134, line 3
