Skip to content

Commit d96666e

Browse files
committed
fix: handle attestations for recompressed images
1 parent 0959572 commit d96666e

File tree

5 files changed

+361
-217
lines changed

5 files changed

+361
-217
lines changed

config/destination.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ type Destination struct {
1616
Path string `yaml:"path" json:"path"`
1717
Package string `yaml:"package" json:"package"`
1818
Platforms []string `yaml:"platforms" json:"platforms"`
19-
20-
platforms map[string]struct{} `yaml:"-" json:"-"`
2119
}
2220

2321
var (
@@ -39,10 +37,6 @@ func (cfg *Destination) Validate() error {
3937
DestinationGcpArtifactRegistryGeneric,
4038
}
4139

42-
destinationsWithPlatform := []string{
43-
DestinationGcpArtifactRegistryDocker,
44-
}
45-
4640
{ // type
4741
if !slices.Contains(allDestinations, cfg.Type) {
4842
errs = append(errs, fmt.Errorf("%w: %s (must be one of: %s)",
@@ -51,31 +45,12 @@ func (cfg *Destination) Validate() error {
5145
}
5246
}
5347

54-
{ // platforms
55-
cfg.platforms = make(map[string]struct{}, len(cfg.Platforms))
56-
if len(cfg.Platforms) > 0 {
57-
if !slices.Contains(destinationsWithPlatform, cfg.Type) {
58-
errs = append(errs, fmt.Errorf("%w: %s",
59-
errDestinationDoesNotSupportPlatforms, cfg.Type,
60-
))
61-
}
62-
63-
for _, platform := range cfg.Platforms {
64-
if _, err := cr.ParsePlatform(platform); err == nil {
65-
cfg.platforms[platform] = struct{}{}
66-
} else {
67-
errs = append(errs, fmt.Errorf("%w: %w",
68-
errDestinationInvalidPlatform, err,
69-
))
70-
}
71-
}
72-
}
73-
}
74-
7548
return utils.FlattenErrors(errs)
7649
}
7750

7851
func (cfg *Destination) HasPlatform(p *cr.Platform) bool {
79-
_, has := cfg.platforms[p.String()] // TODO: enable globbing and matching
80-
return has
52+
if len(cfg.Platforms) == 0 {
53+
return true
54+
}
55+
return slices.Contains(cfg.Platforms, p.String())
8156
}

server/docker.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package server
2+
3+
import (
4+
"archive/zip"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/flashbots/gh-artifacts-sync/config"
12+
"github.com/flashbots/gh-artifacts-sync/job"
13+
"github.com/flashbots/gh-artifacts-sync/logutils"
14+
"github.com/flashbots/gh-artifacts-sync/utils"
15+
"go.uber.org/zap"
16+
17+
crname "github.com/google/go-containerregistry/pkg/name"
18+
cr "github.com/google/go-containerregistry/pkg/v1"
19+
crempty "github.com/google/go-containerregistry/pkg/v1/empty"
20+
crmutate "github.com/google/go-containerregistry/pkg/v1/mutate"
21+
crtarball "github.com/google/go-containerregistry/pkg/v1/tarball"
22+
)
23+
24+
type container struct {
25+
config *cr.ConfigFile
26+
digest cr.Hash
27+
image cr.Image
28+
manifest *cr.Manifest
29+
}
30+
31+
func (s *Server) prepareIndexManifestForDestination(
32+
indexManifest *cr.IndexManifest,
33+
dst *config.Destination,
34+
) error {
35+
if indexManifest == nil || dst == nil {
36+
return nil
37+
}
38+
39+
var (
40+
attestations = make(map[string]*cr.Descriptor, 0)
41+
images = make(map[string]*cr.Descriptor, 0)
42+
errs = make([]error, 0)
43+
)
44+
45+
{ // separate images from their respective attestations
46+
for _, desc := range indexManifest.Manifests {
47+
if desc.Annotations["vnd.docker.reference.type"] != "attestation-manifest" {
48+
images[desc.Digest.String()] = &desc
49+
continue
50+
}
51+
52+
digest := desc.Annotations["vnd.docker.reference.digest"]
53+
if digest == "" {
54+
err := fmt.Errorf("index contains reference w/o digest: %s",
55+
desc.Digest.String(),
56+
)
57+
errs = append(errs, err)
58+
continue
59+
}
60+
61+
if another, collision := attestations[digest]; collision {
62+
err := fmt.Errorf("index contains multiple attestations for the same reference: %s: %s vs. %s",
63+
digest, desc.Digest.String(), another.Digest.String(),
64+
)
65+
errs = append(errs, err)
66+
continue
67+
}
68+
69+
attestations[digest] = &desc
70+
}
71+
}
72+
73+
{ // filter out by platform
74+
for digest, image := range images {
75+
if !dst.HasPlatform(image.Platform) {
76+
delete(images, digest)
77+
delete(attestations, digest)
78+
}
79+
}
80+
}
81+
82+
indexManifest.Manifests = make([]cr.Descriptor, 0, len(images)+len(attestations))
83+
for digest, image := range images {
84+
indexManifest.Manifests = append(indexManifest.Manifests, *image)
85+
indexManifest.Manifests = append(indexManifest.Manifests, *attestations[digest])
86+
}
87+
88+
return utils.FlattenErrors(errs)
89+
}
90+
91+
func (s *Server) prepareImageForUpload(
92+
ctx context.Context,
93+
j job.UploadableContainer,
94+
zname string,
95+
dst *config.Destination,
96+
) (
97+
crname.Reference, cr.Image, cr.ImageIndex, error,
98+
) {
99+
l := logutils.LoggerFromContext(ctx)
100+
101+
var z *zip.ReadCloser
102+
{ // open archive
103+
_z, err := zip.OpenReader(zname)
104+
if err != nil {
105+
return nil, nil, nil, fmt.Errorf("failed to open zip file: %w", err)
106+
}
107+
defer _z.Close()
108+
z = _z
109+
}
110+
111+
errs := make([]error, 0)
112+
113+
var containers = make(map[string]*container, 0)
114+
var indexManifest *cr.IndexManifest
115+
{ // get index and images
116+
for _, f := range z.File {
117+
if f.FileInfo().IsDir() {
118+
continue
119+
}
120+
121+
l := l.With(
122+
zap.String("file", f.Name),
123+
)
124+
125+
switch filepath.Ext(f.Name) {
126+
case ".json":
127+
stream, err := f.Open()
128+
if err != nil {
129+
l.Error("Failed to open index json", zap.Error(err))
130+
errs = append(errs, err)
131+
continue
132+
}
133+
134+
indexManifest = &cr.IndexManifest{}
135+
if err := json.NewDecoder(stream).Decode(indexManifest); err != nil {
136+
l.Error("Failed to decode index json", zap.Error(err))
137+
errs = append(errs, err)
138+
continue
139+
}
140+
141+
case ".tar":
142+
image, err := crtarball.Image(zipFileOpener(f), nil)
143+
if err != nil {
144+
l.Error("Failed to open container tarball", zap.Error(err))
145+
errs = append(errs, err)
146+
continue
147+
}
148+
149+
manifest, err := image.Manifest()
150+
if err != nil {
151+
l.Error("Failed to get container's manifest", zap.Error(err))
152+
errs = append(errs, err)
153+
continue
154+
}
155+
156+
config, err := image.ConfigFile()
157+
if err != nil {
158+
l.Error("Failed to get container's config file", zap.Error(err))
159+
errs = append(errs, err)
160+
continue
161+
}
162+
163+
digest, err := image.Digest()
164+
if err != nil {
165+
l.Error("Failed to get container's digest", zap.Error(err))
166+
errs = append(errs, err)
167+
continue
168+
}
169+
170+
// digest changes depending on the compression used
171+
originalDigest := strings.ReplaceAll(strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)), "-", ":")
172+
173+
containers[originalDigest] = &container{
174+
config: config,
175+
digest: digest,
176+
image: image,
177+
manifest: manifest,
178+
}
179+
}
180+
}
181+
}
182+
183+
{ // filter platforms
184+
switch indexManifest {
185+
case nil:
186+
for originalDigest, container := range containers {
187+
if !dst.HasPlatform(container.config.Platform()) {
188+
delete(containers, originalDigest)
189+
}
190+
}
191+
192+
default:
193+
if err := s.prepareIndexManifestForDestination(indexManifest, dst); err != nil {
194+
errs = append(errs, err)
195+
}
196+
_containers := make(map[string]*container)
197+
for _, desc := range indexManifest.Manifests {
198+
if c := containers[desc.Digest.String()]; c != nil {
199+
_containers[desc.Digest.String()] = c
200+
}
201+
}
202+
containers = _containers
203+
}
204+
}
205+
if len(containers) == 0 {
206+
l.Debug("No matching platforms, skipping...")
207+
return nil, nil, nil, utils.FlattenErrors(errs)
208+
}
209+
210+
var ref crname.Reference
211+
{ // get remote reference
212+
reference := j.GetDestinationReference(dst)
213+
_ref, err := crname.ParseReference(reference)
214+
if err != nil {
215+
l.Error("Failed to parse destination reference",
216+
zap.Error(err),
217+
zap.String("reference", reference),
218+
)
219+
errs = append(errs, err)
220+
return nil, nil, nil, utils.FlattenErrors(errs)
221+
}
222+
ref = _ref
223+
}
224+
225+
switch indexManifest {
226+
case nil:
227+
for _, c := range containers {
228+
// there's only 1 if there's no index
229+
return ref, c.image, nil, utils.FlattenErrors(errs)
230+
}
231+
232+
default:
233+
var index cr.ImageIndex = crempty.Index
234+
for _, desc := range indexManifest.Manifests {
235+
originalDigest := desc.Digest.String()
236+
container := containers[originalDigest]
237+
annotations := desc.Annotations
238+
239+
if annotations["vnd.docker.reference.type"] == "attestation-manifest" {
240+
if annotationOriginalDigest, ok := annotations["vnd.docker.reference.digest"]; ok {
241+
if reference, ok := containers[annotationOriginalDigest]; ok {
242+
annotations["vnd.docker.reference.digest"] = reference.digest.String()
243+
}
244+
}
245+
}
246+
247+
index = crmutate.AppendManifests(index, crmutate.IndexAddendum{
248+
Add: container.image,
249+
250+
Descriptor: cr.Descriptor{
251+
Annotations: annotations,
252+
Digest: container.digest,
253+
Platform: container.config.Platform(),
254+
},
255+
})
256+
}
257+
258+
return ref, nil, index, utils.FlattenErrors(errs)
259+
}
260+
261+
return nil, nil, nil, utils.FlattenErrors(errs)
262+
}

0 commit comments

Comments
 (0)