From 521bf98f58d3f03ec82642c137c090a468c50911 Mon Sep 17 00:00:00 2001 From: Jon Johnson Date: Tue, 10 Dec 2024 10:42:59 -0800 Subject: [PATCH] Support multi-arch config outputs (#421) Prior to this change, apko_config had a single "config" property that contained an apko config, where the package list was the intersection of the arch-specific package lists. Most of the time, the package list is identical across architectures, but architecture-specific dependencies (like libatomic) can lead to divergence. Since we only include the intersection, we would omit these architecture specific packages. When apko_build re-solved the locked package list, it would add the missing packages back, so this worked out, but was suboptimal. We want to solve this mostly because this information being incomplete prevents us from relying on it, but this also solves an annoying hole in our reproducibility story, because the missing packages would not be pinned to any version and could float. In the future, we may want to upgrade these locked versions (e.g. "foo=1.2.3-r4") to locked hashes (e.g. "foo> --- docs/data-sources/config.md | 97 +++++++++ docs/resources/build.md | 97 +++++++++ go.mod | 39 ++-- go.sum | 80 +++---- internal/provider/build.go | 211 +++++++++++++++++++ internal/provider/config_data_source.go | 117 ++++++++-- internal/provider/config_data_source_test.go | 190 +++++++++++++---- internal/provider/resource_build.go | 20 ++ internal/provider/resource_build_test.go | 47 +++++ 9 files changed, 780 insertions(+), 118 deletions(-) diff --git a/docs/data-sources/config.md b/docs/data-sources/config.md index 0e89f8a..0c8da18 100644 --- a/docs/data-sources/config.md +++ b/docs/data-sources/config.md @@ -18,6 +18,7 @@ This reads an apko configuration file into a structured form. ### Optional - `config_contents` (String) The raw contents of the apko configuration. +- `configs` (Attributes Map) A map from the APK architecture to the config for that architecture. (see [below for nested schema](#nestedatt--configs)) - `default_annotations` (Map of String) Default annotations to add. - `extra_packages` (List of String) A list of extra packages to install. @@ -26,6 +27,102 @@ This reads an apko configuration file into a structured form. - `config` (Object) The parsed structure of the apko configuration. (see [below for nested schema](#nestedatt--config)) - `id` (String) A unique identifier for this apko config. + +### Nested Schema for `configs` + +Read-Only: + +- `config` (Object) The parsed structure of the apko configuration. (see [below for nested schema](#nestedatt--configs--config)) + + +### Nested Schema for `configs.config` + +Optional: + +- `accounts` (Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts)) +- `annotations` (Map of String) +- `archs` (List of String) +- `cmd` (String) +- `contents` (Object) (see [below for nested schema](#nestedobjatt--configs--config--contents)) +- `entrypoint` (Object) (see [below for nested schema](#nestedobjatt--configs--config--entrypoint)) +- `environment` (Map of String) +- `include` (String) +- `paths` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--paths)) +- `stop-signal` (String) +- `vcs-url` (String) +- `volumes` (List of String) +- `work-dir` (String) + + +### Nested Schema for `configs.config.accounts` + +Optional: + +- `groups` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts--groups)) +- `run-as` (String) +- `users` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts--users)) + + +### Nested Schema for `configs.config.accounts.groups` + +Optional: + +- `gid` (Number) +- `groupname` (String) +- `members` (List of String) + + + +### Nested Schema for `configs.config.accounts.users` + +Optional: + +- `gid` (Number) +- `homedir` (String) +- `shell` (String) +- `uid` (Number) +- `username` (String) + + + + +### Nested Schema for `configs.config.contents` + +Optional: + +- `build_repositories` (List of String) +- `keyring` (List of String) +- `packages` (List of String) +- `repositories` (List of String) + + + +### Nested Schema for `configs.config.entrypoint` + +Optional: + +- `command` (String) +- `services` (Map of String) +- `shell-fragment` (String) +- `type` (String) + + + +### Nested Schema for `configs.config.paths` + +Optional: + +- `gid` (Number) +- `path` (String) +- `permissions` (Number) +- `recursive` (Boolean) +- `source` (String) +- `type` (String) +- `uid` (Number) + + + + ### Nested Schema for `config` diff --git a/docs/resources/build.md b/docs/resources/build.md index 5d009db..284f9eb 100644 --- a/docs/resources/build.md +++ b/docs/resources/build.md @@ -60,6 +60,7 @@ resource "apko_build" "example" { ### Optional +- `configs` (Attributes Map) A map from the APK architecture to the config for that architecture. (see [below for nested schema](#nestedatt--configs)) - `sboms` (Attributes Map) A map from the APK architecture to the digest for that architecture and its SBOM. (see [below for nested schema](#nestedatt--sboms)) ### Read-Only @@ -155,6 +156,102 @@ Required: + +### Nested Schema for `configs` + +Required: + +- `config` (Object) The parsed structure of the apko configuration. (see [below for nested schema](#nestedatt--configs--config)) + + +### Nested Schema for `configs.config` + +Optional: + +- `accounts` (Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts)) +- `annotations` (Map of String) +- `archs` (List of String) +- `cmd` (String) +- `contents` (Object) (see [below for nested schema](#nestedobjatt--configs--config--contents)) +- `entrypoint` (Object) (see [below for nested schema](#nestedobjatt--configs--config--entrypoint)) +- `environment` (Map of String) +- `include` (String) +- `paths` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--paths)) +- `stop-signal` (String) +- `vcs-url` (String) +- `volumes` (List of String) +- `work-dir` (String) + + +### Nested Schema for `configs.config.accounts` + +Optional: + +- `groups` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts--groups)) +- `run-as` (String) +- `users` (List of Object) (see [below for nested schema](#nestedobjatt--configs--config--accounts--users)) + + +### Nested Schema for `configs.config.accounts.groups` + +Optional: + +- `gid` (Number) +- `groupname` (String) +- `members` (List of String) + + + +### Nested Schema for `configs.config.accounts.users` + +Optional: + +- `gid` (Number) +- `homedir` (String) +- `shell` (String) +- `uid` (Number) +- `username` (String) + + + + +### Nested Schema for `configs.config.contents` + +Optional: + +- `build_repositories` (List of String) +- `keyring` (List of String) +- `packages` (List of String) +- `repositories` (List of String) + + + +### Nested Schema for `configs.config.entrypoint` + +Optional: + +- `command` (String) +- `services` (Map of String) +- `shell-fragment` (String) +- `type` (String) + + + +### Nested Schema for `configs.config.paths` + +Optional: + +- `gid` (Number) +- `path` (String) +- `permissions` (Number) +- `recursive` (Boolean) +- `source` (String) +- `type` (String) +- `uid` (Number) + + + + ### Nested Schema for `sboms` diff --git a/go.mod b/go.mod index 14cd2ec..a1ff9fc 100644 --- a/go.mod +++ b/go.mod @@ -14,17 +14,17 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.10.0 github.com/sigstore/cosign/v2 v2.4.1 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.9.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/apimachinery v0.31.2 + k8s.io/apimachinery v0.31.3 knative.dev/pkg v0.0.0-20240912132815-3002873b449c ) require ( chainguard.dev/go-grpc-kit v0.17.6 // indirect chainguard.dev/sdk v0.1.28 // indirect - cloud.google.com/go/auth v0.9.9 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + cloud.google.com/go/auth v0.10.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -91,6 +91,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect @@ -180,28 +181,28 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect - go.step.sm/crypto v0.54.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect + go.step.sm/crypto v0.54.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.28.0 // indirect + golang.org/x/crypto v0.29.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.26.0 // indirect - google.golang.org/api v0.203.0 // indirect + google.golang.org/api v0.209.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f // indirect google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a2ec3f1..a05b9c4 100644 --- a/go.sum +++ b/go.sum @@ -5,10 +5,10 @@ chainguard.dev/go-grpc-kit v0.17.6/go.mod h1:ZNaXn2KEO++2u2WveHs65krYiHmAEGjYLeE chainguard.dev/sdk v0.1.28 h1:xLQv0JxiGhqVKOL059DmTReTjrKFhUsP5U1W6cgr+jQ= chainguard.dev/sdk v0.1.28/go.mod h1:9EvGI9GY5UPDbZ5AhGbMO8865ixNu36afQYCZ5M95NM= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= -cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= -cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= -cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= +cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= +cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= @@ -221,8 +221,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU= @@ -484,22 +484,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.step.sm/crypto v0.54.0 h1:V8p+12Ld0NRA/RBMYoKXA0dWmVKZSdCwP56IwzweT9g= -go.step.sm/crypto v0.54.0/go.mod h1:vQJyTngfZDW+UyZdFzOMCY/txWDAmcwViEUC7Gn4YfU= +go.step.sm/crypto v0.54.2 h1:3LSA5nYDQvcd484OSx7xsS3XDqQ7/WZjVqvq0+a0fWc= +go.step.sm/crypto v0.54.2/go.mod h1:1+OjUozd5aA3TkBJfr5Aobd6vNt9F70n1DagcoBh3Pc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -516,8 +516,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= @@ -546,11 +546,11 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -558,8 +558,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -584,15 +584,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -602,10 +602,10 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -624,8 +624,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= -google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= +google.golang.org/api v0.209.0 h1:Ja2OXNlyRlWCWu8o+GgI4yUn/wz9h/5ZfFbKz+dQX+w= +google.golang.org/api v0.209.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -634,10 +634,10 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw= -google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g= +google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -658,8 +658,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -682,8 +682,8 @@ gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= -k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= diff --git a/internal/provider/build.go b/internal/provider/build.go index 986bb2f..e22904f 100644 --- a/internal/provider/build.go +++ b/internal/provider/build.go @@ -14,6 +14,7 @@ import ( "chainguard.dev/apko/pkg/build/oci" "chainguard.dev/apko/pkg/build/types" "chainguard.dev/apko/pkg/options" + "chainguard.dev/apko/pkg/tarfs" "github.com/chainguard-dev/clog" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" @@ -83,6 +84,11 @@ type imagesbom struct { } func doBuild(ctx context.Context, data BuildResourceModel, tempDir string) (v1.Hash, coci.SignedEntity, map[string]imagesbom, error) { + // Prefer the new arch-specific configs if they are set. + if len(data.Configs.Elements()) != 0 { + return doNewBuild(ctx, data, tempDir) + } + var ic types.ImageConfiguration if diags := assignValue(data.Config, &ic); diags.HasError() { return v1.Hash{}, nil, nil, fmt.Errorf("assigning value: %v", diags.Errors()) @@ -262,3 +268,208 @@ func doBuild(ctx context.Context, data BuildResourceModel, tempDir string) (v1.H } return h, idx, sboms, nil } + +// doNewBuild is very similar to doBuild, but it uses the (currently options) "configs" input +// to process per-arch apko configs, which allows us to have different locked sets of packages per arch. +// This is important for packages that have different dependencies on each architecture, since +// we can't accurately unify() them. +func doNewBuild(ctx context.Context, data BuildResourceModel, tempDir string) (v1.Hash, coci.SignedEntity, map[string]imagesbom, error) { + byArch := map[string]types.ImageConfiguration{} + + for arch, attr := range data.Configs.Elements() { + var obj struct { + Config types.ImageConfiguration `tfsdk:"config"` + } + if diags := assignValue(attr, &obj); diags.HasError() { + return v1.Hash{}, nil, nil, fmt.Errorf("assigning value: %v", diags.Errors()) + } + + ic := obj.Config + + tflog.Trace(ctx, fmt.Sprintf("Got image configuration for %s: %#v", arch, ic)) + + byArch[arch] = ic + } + + ic, ok := byArch["index"] + if !ok { + return v1.Hash{}, nil, nil, fmt.Errorf("missing index configuration") + } + + // Parse things once to determine the architectures to build from + // the config. + o, ic2, err := fromImageData(ctx, ic, data.popts) + if err != nil { + return v1.Hash{}, nil, nil, err + } + + // We compute the "build date epoch" of the multi-arch image to be the + // maximum "build date epoch" of the per-arch images. If the user has + // explicitly set SOURCE_DATE_EPOCH, that will always trump this + // computation. + multiArchBDE := o.SourceDateEpoch + + var mu sync.Mutex + imgs := make(map[types.Architecture]coci.SignedImage, len(ic2.Archs)) + sboms := make(map[string]imagesbom, len(ic2.Archs)+1) + + var errg errgroup.Group + for _, arch := range ic2.Archs { + arch := arch + + log := clog.New(slog.Default().Handler()).With("arch", arch.ToAPK()) + ctx := clog.WithLogger(ctx, log) + + errg.Go(func() error { + ic, ok := byArch[arch.String()] + if !ok { + return fmt.Errorf("missing arch %q configuration", arch.String()) + } + _, ic2, err := fromImageData(ctx, ic, data.popts) + if err != nil { + return fmt.Errorf("failed to convert image data to config %q: %w", arch, err) + } + + bc, err := build.New(ctx, tarfs.New(), build.WithImageConfiguration(*ic2), + build.WithCache("", false, data.popts.cache), + build.WithSBOMFormats([]string{"spdx"}), + build.WithSBOM(tempDir), + build.WithArch(arch), + build.WithTempDir(tempDir), + build.WithExtraKeys(data.popts.keyring), + build.WithExtraBuildRepos(data.popts.buildRespositories), + build.WithExtraRuntimeRepos(data.popts.repositories)) + if err != nil { + return fmt.Errorf("failed to start apko build: %w", err) + } + + _, layer, err := bc.BuildLayer(ctx) + if err != nil { + return fmt.Errorf("failed to build layer image for %q: %w", arch, err) + } + + bde, err := bc.GetBuildDateEpoch() + if err != nil { + return fmt.Errorf("failed to determine build date epoch: %w", err) + } + + img, err := oci.BuildImageFromLayer(ctx, empty.Image, layer, bc.ImageConfiguration(), bde, bc.Arch()) + if err != nil { + return fmt.Errorf("failed to build OCI image for %q: %w", arch, err) + } + + outputs, err := bc.GenerateImageSBOM(ctx, arch, img) + if err != nil { + return fmt.Errorf("generating sbom for %s: %w", arch, err) + } + + h, err := img.Digest() + if err != nil { + return fmt.Errorf("unable to compute digest for %q: %w", arch, err) + } + + // We have hardcoded sbom formats to be just "spdx", fail if this isn't right. + if len(outputs) != 1 { + return fmt.Errorf("saw %d sbom outputs, expected 1", len(outputs)) + } + + // Move the sbom to a temporary file outside of the directory we + // plan to clean up, so that it outlives the evaluation of this + // build resource. + sbomPath := outputs[0].Path + f, err := os.CreateTemp("", "sbom-*.spdx.json") + if err != nil { + return fmt.Errorf("unable to create temporary file for sbom: %w", err) + } + defer f.Close() + + content, err := os.ReadFile(sbomPath) + if err != nil { + return fmt.Errorf("unable to read SBOM %q: %w", arch, err) + } + if _, err := f.Write(content); err != nil { + return fmt.Errorf("failed to write sbom to %q: %w", f.Name(), err) + } + hash := sha256.Sum256(content) + + mu.Lock() + defer mu.Unlock() + + // Adjust the index's builder to track the most recent BDE. + if bde.After(multiArchBDE) { + multiArchBDE = bde + } + + // save the images for later + imgs[arch] = img + + sboms[arch.String()] = imagesbom{ + imageHash: h, + predicateType: "https://spdx.dev/Document", + predicatePath: f.Name(), + predicateSHA256: hex.EncodeToString(hash[:]), + } + + return nil + }) + } + + if err := errg.Wait(); err != nil { + return v1.Hash{}, nil, nil, err + } + + // generate the index + finalDigest, idx, err := oci.GenerateIndex(ctx, *ic2, imgs, multiArchBDE) + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("failed to generate OCI index: %w", err) + } + + o, ic2, err = build.NewOptions( + build.WithImageConfiguration(*ic2), // We mutate Archs above. + build.WithSourceDateEpoch(multiArchBDE), // Maximum child's time. + build.WithSBOMFormats([]string{"spdx"}), + build.WithSBOM(tempDir), + build.WithExtraKeys(data.popts.keyring), + build.WithExtraRuntimeRepos(data.popts.repositories), + build.WithExtraBuildRepos(data.popts.buildRespositories), + ) + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("failed to create options for index: %w", err) + } + + isboms, err := build.GenerateIndexSBOM(ctx, *o, *ic2, finalDigest, imgs) + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("generating index SBOM: %w", err) + } + + // Move the sbom to a temporary file outside of the directory we + // plan to clean up, so that it outlives the evaluation of this + // build resource. + sbomPath := isboms[0].Path + f, err := os.CreateTemp("", "sbom-*.spdx.json") + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("unable to create temporary file for sbom: %w", err) + } + defer f.Close() + content, err := os.ReadFile(sbomPath) + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("unable to read index SBOM: %w", err) + } + if _, err := f.Write(content); err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("failed to write sbom to %q: %w", f.Name(), err) + } + hash := sha256.Sum256(content) + + h, err := idx.Digest() + if err != nil { + return v1.Hash{}, nil, nil, fmt.Errorf("unable to compute digest for index: %w", err) + } + + sboms["index"] = imagesbom{ + imageHash: h, + predicateType: "https://spdx.dev/Document", + predicatePath: f.Name(), + predicateSHA256: hex.EncodeToString(hash[:]), + } + return h, idx, sboms, nil +} diff --git a/internal/provider/config_data_source.go b/internal/provider/config_data_source.go index 05c30a3..ad9e386 100644 --- a/internal/provider/config_data_source.go +++ b/internal/provider/config_data_source.go @@ -15,6 +15,7 @@ import ( "chainguard.dev/apko/pkg/build" apkotypes "chainguard.dev/apko/pkg/build/types" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -44,11 +45,13 @@ type ConfigDataSourceModel struct { Id types.String `tfsdk:"id"` ConfigContents types.String `tfsdk:"config_contents"` Config types.Object `tfsdk:"config"` + Configs types.Map `tfsdk:"configs"` ExtraPackages []string `tfsdk:"extra_packages"` DefaultAnnotations map[string]string `tfsdk:"default_annotations"` } var imageConfigurationSchema basetypes.ObjectType +var imageConfigurationsSchema basetypes.ObjectType func init() { sch, err := generateType(apkotypes.ImageConfiguration{}) @@ -61,6 +64,12 @@ func init() { if !ok { panic("expected object type") } + + imageConfigurationsSchema = basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "config": imageConfigurationSchema, + }, + } } func (d *ConfigDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -80,6 +89,21 @@ func (d *ConfigDataSource) Schema(ctx context.Context, req datasource.SchemaRequ Computed: true, AttributeTypes: imageConfigurationSchema.AttrTypes, }, + "configs": schema.MapNestedAttribute{ + MarkdownDescription: "A map from the APK architecture to the config for that architecture.", + Computed: true, + Optional: true, + Required: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "config": schema.ObjectAttribute{ + MarkdownDescription: "The parsed structure of the apko configuration.", + Computed: true, + AttributeTypes: imageConfigurationSchema.AttrTypes, + }, + }, + }, + }, "extra_packages": schema.ListAttribute{ MarkdownDescription: "A list of extra packages to install.", Optional: true, @@ -176,32 +200,70 @@ func (d *ConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Resolve the package list to specific versions (as much as we can with // multi-arch), and overwrite the package list in the ImageConfiguration. - pl, diags := d.resolvePackageList(ctx, ic) + pls, diags := d.resolvePackageList(ctx, ic) resp.Diagnostics = append(resp.Diagnostics, diags...) if diags.HasError() { return } - ic.Contents.Packages = pl - if out := os.Getenv("TF_APKO_OUT_DIR"); out != "" { - if err := writeFile(out, hash, "post", ic); err != nil { - resp.Diagnostics.AddError("Unable to write apko configuration", err.Error()) + cfgMap := make(map[string]attr.Value) + + for arch, pl := range pls { + // Create a defensive copy of "ic". + copied := apkotypes.ImageConfiguration{} + if err := ic.MergeInto(&copied); err != nil { + resp.Diagnostics.AddError("Unable to write apko configuration", "copying image configuration: "+err.Error()) return } - } - ov, diags := generateValue(ic) - resp.Diagnostics = append(resp.Diagnostics, diags...) - if diags.HasError() { - return + copied.Contents.Packages = pl + + if arch != "index" { + // Overwrite single-arch configs with their specific arch. + copied.Archs = []apkotypes.Architecture{apkotypes.ParseArchitecture(arch)} + } + + ov, diags := generateValue(copied) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diags.HasError() { + return + } + + cfg, ok := ov.(basetypes.ObjectValue) + if !ok { + resp.Diagnostics.AddError("Unable to write apko configuration", "unexpected object type or malformed object type") + return + } + + // Keep original behavior for "apko_config.config" that only uses only the merged "index" arch. + if arch == "index" { + if out := os.Getenv("TF_APKO_OUT_DIR"); out != "" { + if err := writeFile(out, hash, "post", copied); err != nil { + resp.Diagnostics.AddError("Unable to write apko configuration", err.Error()) + return + } + } + + data.Config = cfg + } + + val, diags := types.ObjectValue(imageConfigurationsSchema.AttrTypes, map[string]attr.Value{ + "config": cfg, + }) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diags.HasError() { + return + } + + cfgMap[arch] = val } - var ok bool - data.Config, ok = ov.(basetypes.ObjectValue) - if !ok { - resp.Diagnostics.AddError("Unable to write apko configuration", "unexpected object type or malformed object type") + cfgMapValue, diags := types.MapValue(imageConfigurationsSchema, cfgMap) + if diags != nil { + resp.Diagnostics = append(resp.Diagnostics, diags...) return } + data.Configs = cfgMapValue data.Id = types.StringValue(hash) @@ -221,7 +283,7 @@ func writeFile(dir, hash, variant string, ic apkotypes.ImageConfiguration) error return os.WriteFile(filepath.Join(dir, fn), b, 0644) } -func (d *ConfigDataSource) resolvePackageList(ctx context.Context, ic apkotypes.ImageConfiguration) ([]string, diag.Diagnostics) { +func (d *ConfigDataSource) resolvePackageList(ctx context.Context, ic apkotypes.ImageConfiguration) (map[string][]string, diag.Diagnostics) { _, ic2, err := fromImageData(ctx, ic, d.popts) if err != nil { return nil, diag.Diagnostics{diag.NewErrorDiagnostic("Unable to parse apko config", err.Error())} @@ -300,14 +362,21 @@ type resolved struct { provided map[string]sets.Set[string] } -func unify(originals []string, inputs []resolved) ([]string, diag.Diagnostics) { +func unify(originals []string, inputs []resolved) (map[string][]string, diag.Diagnostics) { if len(originals) == 0 { - return nil, nil + // If there are no original packages, then we can't really do anything. + // This used to return nil but multi-arch unification assumes we always + // have an "index" entry, even if it's empty, so we return this now. + // Mostly this is to satisfy some tests that have no package inputs. + return map[string][]string{"index": {}}, nil } originalPackages := resolved{ packages: make(sets.Set[string], len(originals)), versions: make(map[string]string, len(originals)), } + + byArch := map[string][]string{} + for _, orig := range originals { name := orig // The function we want from go-apk is private, but these are all the @@ -422,6 +491,18 @@ func unify(originals []string, inputs []resolved) ([]string, diag.Diagnostics) { pl = append(pl, fmt.Sprintf("%s=%s", pkg, acc.versions[pkg])) } + // "index" is a sentinel value for our multi-arch image index. + // This mirrors how "sboms" works in the BuildResource. + byArch["index"] = pl + + for _, input := range inputs { + pl := make([]string, 0, len(input.packages)) + for _, pkg := range sets.List(input.packages) { + pl = append(pl, fmt.Sprintf("%s=%s", pkg, input.versions[pkg])) + } + byArch[input.arch] = pl + } + // If a particular architecture is missing additional packages from the // locked set that it produced, than warn about those as well. for _, input := range inputs { @@ -434,7 +515,7 @@ func unify(originals []string, inputs []resolved) ([]string, diag.Diagnostics) { } } - return pl, diagnostics + return byArch, diagnostics } // Copied from go-apk's version.go. diff --git a/internal/provider/config_data_source_test.go b/internal/provider/config_data_source_test.go index b30c769..53a94e2 100644 --- a/internal/provider/config_data_source_test.go +++ b/internal/provider/config_data_source_test.go @@ -227,14 +227,16 @@ func TestUnify(t *testing.T) { name string originals []string inputs []resolved - want []string + want map[string][]string wantDiag diag.Diagnostics }{{ name: "empty", + want: map[string][]string{"index": {}}, }, { name: "simple single arch", originals: []string{"foo", "bar", "baz"}, inputs: []resolved{{ + arch: "amd64", packages: sets.New("foo", "bar", "baz"), versions: map[string]string{ "foo": "1.2.3", @@ -242,15 +244,23 @@ func TestUnify(t *testing.T) { "baz": "0.0.1", }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, }, }, { name: "locked versions", originals: []string{"foo=1.2.3", "bar=2.4.6", "baz=0.0.1"}, inputs: []resolved{{ + arch: "amd64", packages: sets.New("foo", "bar", "baz"), versions: map[string]string{ "foo": "1.2.3", @@ -258,15 +268,23 @@ func TestUnify(t *testing.T) { "baz": "0.0.1", }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, }, }, { name: "transitive dependency", originals: []string{"foo", "bar", "baz"}, inputs: []resolved{{ + arch: "amd64", packages: sets.New("foo", "bar", "baz", "bonus"), versions: map[string]string{ "foo": "1.2.3", @@ -275,11 +293,19 @@ func TestUnify(t *testing.T) { "bonus": "5.4.3", }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "bonus=5.4.3", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, }, }, { name: "multiple matching architectures", @@ -311,11 +337,25 @@ func TestUnify(t *testing.T) { "bar": sets.New("def", "ogg"), }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "bonus=5.4.3", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "arm64": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, }, }, { name: "mismatched transitive dependency", @@ -339,10 +379,24 @@ func TestUnify(t *testing.T) { "bonus": "5.4.3-r1", }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3-r0", + "foo=1.2.3", + }, + "arm64": { + "bar=2.4.6", + "baz=0.0.1", + "bonus=5.4.3-r1", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, }, wantDiag: []diag.Diagnostic{ diag.NewWarningDiagnostic("unable to lock certain packages for amd64", "[bonus]"), @@ -374,10 +428,22 @@ func TestUnify(t *testing.T) { "bonus": sets.New("bar"), }, }}, - want: []string{ - "baz=0.0.1", - "bonus=5.4.3", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "arm64": { + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "index": { + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, }, }, { name: "mismatched direct dependency", @@ -401,11 +467,25 @@ func TestUnify(t *testing.T) { "bonus": "5.4.3", }, }}, - want: []string{ - "bar", - "baz=0.0.1", - "bonus=5.4.3", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6-r0", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "arm64": { + "bar=2.4.6-r1", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "index": { + "bar", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, }, wantDiag: []diag.Diagnostic{ diag.NewErrorDiagnostic( @@ -435,11 +515,25 @@ func TestUnify(t *testing.T) { "bonus": "5.4.3", }, }}, - want: []string{ - "bar>2.4.6", // Check that we keep our input constraint - "baz=0.0.1", - "bonus=5.4.3", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6-r0", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "arm64": { + "bar=2.4.6-r1", + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, + "index": { + "bar>2.4.6", // Check that we keep our input constraint + "baz=0.0.1", + "bonus=5.4.3", + "foo=1.2.3", + }, }, wantDiag: []diag.Diagnostic{ diag.NewErrorDiagnostic( @@ -469,10 +563,24 @@ func TestUnify(t *testing.T) { "arm-energy-efficient-as-f-arithmetic": "9.8.7", }, }}, - want: []string{ - "bar=2.4.6", - "baz=0.0.1", - "foo=1.2.3", + want: map[string][]string{ + "amd64": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + "intel-fast-as-f-math=5.4.3", + }, + "arm64": { + "arm-energy-efficient-as-f-arithmetic=9.8.7", + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, + "index": { + "bar=2.4.6", + "baz=0.0.1", + "foo=1.2.3", + }, }, wantDiag: []diag.Diagnostic{ diag.NewWarningDiagnostic("unable to lock certain packages for amd64", "[intel-fast-as-f-math]"), diff --git a/internal/provider/resource_build.go b/internal/provider/resource_build.go index 72312a8..a9f6aa0 100644 --- a/internal/provider/resource_build.go +++ b/internal/provider/resource_build.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" @@ -36,6 +37,7 @@ type BuildResourceModel struct { Id types.String `tfsdk:"id"` Repo types.String `tfsdk:"repo"` Config types.Object `tfsdk:"config"` + Configs types.Map `tfsdk:"configs"` ImageRef types.String `tfsdk:"image_ref"` SBOMs types.Map `tfsdk:"sboms"` @@ -99,6 +101,24 @@ func (r *BuildResource) Schema(ctx context.Context, req resource.SchemaRequest, objectplanmodifier.RequiresReplace(), }, }, + "configs": schema.MapNestedAttribute{ + MarkdownDescription: "A map from the APK architecture to the config for that architecture.", + Optional: true, + Required: false, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "config": schema.ObjectAttribute{ + Optional: false, + Required: true, + MarkdownDescription: "The parsed structure of the apko configuration.", + AttributeTypes: imageConfigurationSchema.AttrTypes, + }, + }, + }, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, "image_ref": schema.StringAttribute{ MarkdownDescription: "The resulting fully-qualified digest (e.g. {repo}@sha256:deadbeef).", Computed: true, diff --git a/internal/provider/resource_build_test.go b/internal/provider/resource_build_test.go index 0bc53c4..7bdc3a3 100644 --- a/internal/provider/resource_build_test.go +++ b/internal/provider/resource_build_test.go @@ -382,3 +382,50 @@ resource "apko_build" "foo" { }, }) } + +// Test that this things builds if you give it "configs". +func TestAccResourceApkoBuild_PerArchConfigs(t *testing.T) { + repo, cleanup := ocitesting.SetupRepository(t, "test") + defer cleanup() + + repostr := repo.String() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "apko": providerserver.NewProtocol6WithError(&Provider{ + repositories: []string{"https://packages.wolfi.dev/os"}, + buildRespositories: []string{"./packages"}, + keyring: []string{"https://packages.wolfi.dev/os/wolfi-signing.rsa.pub"}, + archs: []string{"x86_64", "aarch64"}, + packages: []string{"wolfi-baselayout"}, + }), + }, Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +data "apko_config" "foo" { + config_contents = <