diff --git a/src/cmd/linuxkit/cache/push.go b/src/cmd/linuxkit/cache/push.go index e280dee63..bd8f3bf55 100644 --- a/src/cmd/linuxkit/cache/push.go +++ b/src/cmd/linuxkit/cache/push.go @@ -7,7 +7,7 @@ import ( namepkg "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/validate" - "github.com/linuxkit/linuxkit/src/cmd/linuxkit/registry" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" imagespec "github.com/opencontainers/image-spec/specs-go/v1" log "github.com/sirupsen/logrus" ) @@ -57,10 +57,26 @@ func (p *Provider) Push(name string, withManifest bool) error { if err != nil { return fmt.Errorf("could not get digest for index %s: %v", name, err) } + manifest, err := ii.IndexManifest() + if err != nil { + return fmt.Errorf("could not read images in index: %v", err) + } + + // get the existing image, if any desc, err := remote.Get(ref, remoteOptions...) - if err == nil && desc != nil && dig == desc.Digest { - fmt.Printf("%s index already available on remote registry, skipping push", name) - return nil + if err == nil && desc != nil { + if dig == desc.Digest { + fmt.Printf("%s index already available on remote registry, skipping push", name) + return nil + } + // we have a different index, need to cross-reference and only override relevant stuff + remoteIndex, err := desc.ImageIndex() + if err == nil && remoteIndex != nil { + ii, err = util.AppendIndex(ii, remoteIndex) + if err != nil { + return fmt.Errorf("could not append remote index to local index: %v", err) + } + } } log.Debugf("pushing index %s", name) // this is an index, so we not only want to write the index, but tags for each arch-specific image in it @@ -68,10 +84,6 @@ func (p *Provider) Push(name string, withManifest bool) error { return err } fmt.Printf("Pushed index %s\n", name) - manifest, err := ii.IndexManifest() - if err != nil { - return fmt.Errorf("successfully pushed index, but could not read images in index: %v", err) - } log.Debugf("pushing individual images in the index %s", name) for _, m := range manifest.Manifests { if m.Platform == nil || m.Platform.Architecture == "" { @@ -113,17 +125,5 @@ func (p *Provider) Push(name string, withManifest bool) error { return fmt.Errorf("name %s unknown in cache", name) } - if !withManifest { - return nil - } - // Even though we may have pushed the index, we want to be sure that we have an index that includes every architecture on the registry, - // not just those that were in our local cache. So we call PushManifest to push an index that includes all arch-specific images - // already in the registry. - fmt.Printf("Pushing index based on all arch-specific images in registry %s\n", name) - _, _, err = registry.PushManifest(name, options...) - if err != nil { - return err - } - return nil } diff --git a/src/cmd/linuxkit/cache/source.go b/src/cmd/linuxkit/cache/source.go index 3c86f75c7..0a3fa60a9 100644 --- a/src/cmd/linuxkit/cache/source.go +++ b/src/cmd/linuxkit/cache/source.go @@ -15,16 +15,12 @@ import ( "github.com/google/go-containerregistry/pkg/v1/tarball" intoto "github.com/in-toto/in-toto-golang/in_toto" lktspec "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" imagespec "github.com/opencontainers/image-spec/specs-go/v1" ) const ( - annotationDockerReferenceType = "vnd.docker.reference.type" - annotationAttestationManifest = "attestation-manifest" - annotationDockerReferenceDigest = "vnd.docker.reference.digest" - annotationInTotoPredicateType = "in-toto.io/predicate-type" - annotationSPDXDoc = "https://spdx.dev/Document" - inTotoJsonMediaType = "application/vnd.in-toto+json" + inTotoJsonMediaType = "application/vnd.in-toto+json" ) // ImageSource a source for an image in the OCI distribution cache. @@ -143,8 +139,8 @@ func (c ImageSource) SBoMs() ([]io.ReadCloser, error) { desc := descs[0] annotations := map[string]string{ - annotationDockerReferenceType: annotationAttestationManifest, - annotationDockerReferenceDigest: desc.Digest.String(), + util.AnnotationDockerReferenceType: util.AnnotationAttestationManifest, + util.AnnotationDockerReferenceDigest: desc.Digest.String(), } descs, err = partial.FindManifests(index, matchAllAnnotations(annotations)) if err != nil { @@ -183,7 +179,7 @@ func (c ImageSource) SBoMs() ([]io.ReadCloser, error) { var readers []io.ReadCloser for i, layer := range manifest.Layers { annotations := layer.Annotations - if annotations[annotationInTotoPredicateType] != annotationSPDXDoc || layer.MediaType != inTotoJsonMediaType { + if annotations[util.AnnotationInTotoPredicateType] != util.AnnotationSPDXDoc || layer.MediaType != inTotoJsonMediaType { continue } // get the actual blob of the layer @@ -201,7 +197,7 @@ func (c ImageSource) SBoMs() ([]io.ReadCloser, error) { if err := json.Unmarshal(buf.Bytes(), &stmt); err != nil { return nil, err } - if stmt.PredicateType != annotationSPDXDoc { + if stmt.PredicateType != util.AnnotationSPDXDoc { return nil, fmt.Errorf("unexpected predicate type %s", stmt.PredicateType) } sbom := stmt.Predicate diff --git a/src/cmd/linuxkit/cache/write.go b/src/cmd/linuxkit/cache/write.go index 1903b2477..e8008a610 100644 --- a/src/cmd/linuxkit/cache/write.go +++ b/src/cmd/linuxkit/cache/write.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" lktspec "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec" + lktutil "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" imagespec "github.com/opencontainers/image-spec/specs-go/v1" log "github.com/sirupsen/logrus" ) @@ -320,7 +321,7 @@ func (p *Provider) IndexWrite(ref *reference.Spec, descriptors ...v1.Descriptor) appliedManifests[m.Digest] = true continue } - value, ok := m.Annotations[annotationDockerReferenceDigest] + value, ok := m.Annotations[lktutil.AnnotationDockerReferenceDigest] if !ok { manifest.Manifests = append(manifest.Manifests, m) appliedManifests[m.Digest] = true diff --git a/src/cmd/linuxkit/registry/manifest.go b/src/cmd/linuxkit/registry/manifest.go index 7156d9664..c9c838a1a 100644 --- a/src/cmd/linuxkit/registry/manifest.go +++ b/src/cmd/linuxkit/registry/manifest.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" log "github.com/sirupsen/logrus" ) @@ -30,6 +31,7 @@ func PushManifest(img string, options ...remote.Option) (hash string, length int if err != nil { return hash, length, fmt.Errorf("parsing %s: %w", img, err) } + adds := make([]mutate.IndexAddendum, 0, len(platformsToSearchForIndex)) for i, platform := range platformsToSearchForIndex { osArchArr := strings.Split(platform, "/") @@ -70,6 +72,20 @@ func PushManifest(img string, options ...remote.Option) (hash string, length int // add the desc to the index we will push index := mutate.AppendManifests(empty.Index, adds...) + // base index with which we are working + // get the existing index, if any + desc, err := remote.Get(baseRef, options...) + if err == nil && desc != nil { + ii, err := desc.ImageIndex() + if err != nil { + return hash, length, fmt.Errorf("could not get index for existing reference %s: %w", img, err) + } + index, err = util.AppendIndex(index, ii) + if err != nil { + return hash, length, fmt.Errorf("could not append existing index for %s: %w", img, err) + } + } + size, err := index.Size() if err != nil { return hash, length, fmt.Errorf("getting index size: %w", err) diff --git a/src/cmd/linuxkit/util/index.go b/src/cmd/linuxkit/util/index.go new file mode 100644 index 000000000..02e16bf77 --- /dev/null +++ b/src/cmd/linuxkit/util/index.go @@ -0,0 +1,97 @@ +package util + +import ( + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +const ( + AnnotationDockerReferenceDigest = "vnd.docker.reference.digest" + AnnotationDockerReferenceType = "vnd.docker.reference.type" + AnnotationAttestationManifest = "attestation-manifest" + AnnotationInTotoPredicateType = "in-toto.io/predicate-type" + AnnotationSPDXDoc = "https://spdx.dev/Document" +) + +// AppendIndex appends the elements of secondary ImageIndex into primary ImageIndex, +// returning the updated primary ImageIndex. +// In the case of conflicts, the primary ImageIndex wins. +// For example, if both have a manifest for a specific platform, then use the one from primary. +// The append is aware of the buildkit-style attestations, and will keep any attestations that point to a valid +// manifest in the list, discarding any that do not. +func AppendIndex(primary, secondary v1.ImageIndex) (v1.ImageIndex, error) { + primaryManifest, err := primary.IndexManifest() + if err != nil { + return nil, err + } + secondaryManifest, err := secondary.IndexManifest() + if err != nil { + return nil, err + } + // figure out what already is in the index, and what should be overwritten + // what should be checked in the existing index: + // 1. platform - if it is in remote index but not in local, add to local + // 2. attestation - after all platforms, does it point to something in the updated index? + // If not, remove + + // make a map of all the digests already in the index, so we can know what is there + var ( + manifestMap = map[v1.Hash]bool{} + platformMap = map[string]bool{} + ) + for _, m := range primaryManifest.Manifests { + if m.Platform == nil || m.Platform.Architecture == "" { + continue + } + platformKey := fmt.Sprintf("%s/%s/%s", m.Platform.Architecture, m.Platform.OS, m.Platform.Variant) + manifestMap[m.Digest] = true + platformMap[platformKey] = true + } + + for _, m := range secondaryManifest.Manifests { + // ignore any of those without a platform for this run (we will deal witb attestations in a second pass) + if m.Platform == nil || m.Platform.Architecture == "" || (m.Platform.Architecture == "unknown" && m.Platform.OS == "unknown") { + continue + } + platformKey := fmt.Sprintf("%s/%s/%s", m.Platform.Architecture, m.Platform.OS, m.Platform.Variant) + // primary wins if we already have this platform covered + if _, ok := platformMap[platformKey]; ok { + continue + } + if _, ok := manifestMap[m.Digest]; ok { + // we already have this one, so we can skip it + continue + } + primaryManifest.Manifests = append(primaryManifest.Manifests, m) + manifestMap[m.Digest] = true + } + + // now we have assured that all of the images in the remote index are in the local index + // or overridden by matching local ones + // next we have to make sure that any sboms already on the remote index are still valid + // we either add them to the local index, or remove them if they are no longer valid + // we assume the ones in the local index are valid because they would have been generated now + for _, m := range secondaryManifest.Manifests { + if m.Platform == nil || m.Platform.Architecture != "unknown" || m.Platform.OS == "unknown" || m.Annotations == nil || m.Annotations[AnnotationDockerReferenceDigest] == "" { + continue + } + // if we already have this one, we are good + if _, ok := manifestMap[m.Digest]; ok { + continue + } + // the hash to which this attestation points + hash := m.Annotations[AnnotationDockerReferenceDigest] + dig, err := v1.NewHash(hash) + if err != nil { + return nil, fmt.Errorf("could not parse hash %s: %v", hash, err) + } + // if this points at something not in the local index, do not bother adding it + if _, ok := manifestMap[dig]; !ok { + continue + } + primaryManifest.Manifests = append(primaryManifest.Manifests, m) + manifestMap[m.Digest] = true + } + return primary, nil +}