refactor! remove pkg directory (#145)
* refactor!: remove pkg directory * chore: include breaking changes in draft
This commit is contained in:
2
oci/README.md
Normal file
2
oci/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## oci
|
||||
This package is for generic OCI components. For attestation specific components see the `attestation` package.
|
||||
21
oci/authn.go
Normal file
21
oci/authn.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/v1/google"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
func MultiKeychainOption() remote.Option {
|
||||
return remote.WithAuthFromKeychain(MultiKeychainAll())
|
||||
}
|
||||
|
||||
func MultiKeychainAll() authn.Keychain {
|
||||
// Create a multi-keychain that will use the default Docker, Google, or ECR keychain
|
||||
return authn.NewMultiKeychain(
|
||||
authn.DefaultKeychain,
|
||||
google.Keychain,
|
||||
authn.NewKeychainFromHelper(ecr.NewECRHelper()),
|
||||
)
|
||||
}
|
||||
31
oci/authn_test.go
Normal file
31
oci/authn_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
//go:build e2e
|
||||
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/oci"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistryAuth(t *testing.T) {
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
|
||||
require.NoError(t, err)
|
||||
// test cases for ecr, gcr and dockerhub
|
||||
testCases := []struct {
|
||||
Image string
|
||||
}{
|
||||
{Image: "175142243308.dkr.ecr.us-east-1.amazonaws.com/e2e-test-image:latest"},
|
||||
{Image: "docker/image-signer-verifier-test:latest"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Image, func(t *testing.T) {
|
||||
err := oci.PushIndexToRegistry(attIdx.Index, tc.Image)
|
||||
require.NoError(t, err)
|
||||
_, err = oci.IndexFromRemote(tc.Image)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
27
oci/http.go
Normal file
27
oci/http.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
)
|
||||
|
||||
type userAgentTransporter struct {
|
||||
userAgent string
|
||||
roundTripper http.RoundTripper
|
||||
}
|
||||
|
||||
type Option = func(*http.Client)
|
||||
|
||||
func (u *userAgentTransporter) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", u.userAgent)
|
||||
|
||||
return u.roundTripper.RoundTrip(req)
|
||||
}
|
||||
|
||||
func HTTPTransport() http.RoundTripper {
|
||||
return &userAgentTransporter{
|
||||
userAgent: "Docker-Client",
|
||||
roundTripper: cleanhttp.DefaultTransport(),
|
||||
}
|
||||
}
|
||||
149
oci/oci.go
Normal file
149
oci/oci.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/package-url/packageurl-go"
|
||||
)
|
||||
|
||||
// ParsePlatform parses the provided platform string or attempts to obtain
|
||||
// the platform of the current host system.
|
||||
func ParsePlatform(platformStr string) (*v1.Platform, error) {
|
||||
if platformStr == "" {
|
||||
cdp := platforms.Normalize(platforms.DefaultSpec())
|
||||
if cdp.OS != "windows" {
|
||||
cdp.OS = "linux"
|
||||
}
|
||||
return &v1.Platform{
|
||||
OS: cdp.OS,
|
||||
Architecture: cdp.Architecture,
|
||||
Variant: cdp.Variant,
|
||||
}, nil
|
||||
}
|
||||
return v1.ParsePlatform(platformStr)
|
||||
}
|
||||
|
||||
func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||
// prepare options
|
||||
options := []remote.Option{MultiKeychainOption(), remote.WithTransport(HTTPTransport()), remote.WithContext(ctx)}
|
||||
|
||||
// add in platform into remote Get operation; this might conflict with an explicit digest, but we are trying anyway
|
||||
if platform != nil {
|
||||
options = append(options, remote.WithPlatform(*platform))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no image found for platform %v", platform)
|
||||
}
|
||||
|
||||
func RefToPURL(named reference.Named, platform *v1.Platform) (string, bool, error) {
|
||||
var isCanonical bool
|
||||
var qualifiers []packageurl.Qualifier
|
||||
|
||||
if canonical, ok := named.(reference.Canonical); ok {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "digest",
|
||||
Value: canonical.Digest().String(),
|
||||
})
|
||||
isCanonical = true
|
||||
} else {
|
||||
named = reference.TagNameOnly(named)
|
||||
}
|
||||
|
||||
version := ""
|
||||
if tagged, ok := named.(reference.Tagged); ok {
|
||||
version = tagged.Tag()
|
||||
}
|
||||
|
||||
name := reference.FamiliarName(named)
|
||||
|
||||
ns := ""
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) > 1 {
|
||||
ns = strings.Join(parts[:len(parts)-1], "/")
|
||||
}
|
||||
name = parts[len(parts)-1]
|
||||
|
||||
if platform != nil {
|
||||
qualifiers = append(qualifiers, packageurl.Qualifier{
|
||||
Key: "platform",
|
||||
Value: platform.String(),
|
||||
})
|
||||
}
|
||||
|
||||
p := packageurl.NewPackageURL("docker", ns, name, version, qualifiers, "")
|
||||
return p.ToString(), isCanonical, nil
|
||||
}
|
||||
|
||||
func SplitDigest(digest string) (common.DigestSet, error) {
|
||||
parts := strings.SplitN(digest, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid digest %q", digest)
|
||||
}
|
||||
return common.DigestSet{
|
||||
parts[0]: parts[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
newName, err := ReplaceTag(src.Identifier, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||
}
|
||||
return &ImageSpec{
|
||||
Identifier: newName,
|
||||
Type: src.Type,
|
||||
Platform: src.Platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it.
|
||||
func ReplaceTag(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
notag, err := WithoutTag(image)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("%s:%s-%s.att", notag, digest.Algorithm, digest.Hex), nil
|
||||
}
|
||||
|
||||
func ReplaceDigestInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
newName, err := replaceDigest(src.Identifier, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||
}
|
||||
return &ImageSpec{
|
||||
Identifier: newName,
|
||||
Type: src.Type,
|
||||
Platform: src.Platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func replaceDigest(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
notag, err := WithoutTag(image)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("%s@%s:%s", notag, digest.Algorithm, digest.Hex), nil
|
||||
}
|
||||
146
oci/oci_test.go
Normal file
146
oci/oci_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRefToPurl(t *testing.T) {
|
||||
arm, err := oci.ParsePlatform("arm64/linux")
|
||||
require.NoError(t, err)
|
||||
ref, err := reference.ParseNormalizedNamed("alpine")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err := oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("google/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("docker.io/library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/library/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine:123")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
ref, err = reference.ParseNormalizedNamed("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err = oci.RefToPURL(ref, arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
||||
assert.True(t, canonical)
|
||||
}
|
||||
|
||||
// Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202
|
||||
func TestImageDigestForPlatform(t *testing.T) {
|
||||
idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage(".."))
|
||||
assert.NoError(t, err)
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
idxDescriptor := idxm.Manifests[0]
|
||||
idxDigest := idxDescriptor.Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
assert.NoError(t, err)
|
||||
mfs2, err := mfs.IndexManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
p, err := oci.ParsePlatform("linux/amd64")
|
||||
assert.NoError(t, err)
|
||||
desc, err := oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
||||
|
||||
p, err = oci.ParsePlatform("linux/arm64")
|
||||
assert.NoError(t, err)
|
||||
desc, err = oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest = desc.Digest.String()
|
||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||
}
|
||||
|
||||
func TestWithoutTag(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{name: "image:tag", expected: "index.docker.io/library/image"},
|
||||
{name: "image", expected: "index.docker.io/library/image"},
|
||||
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"},
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
notag, _ := oci.WithoutTag(c.name)
|
||||
assert.Equal(t, c.expected, notag)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceTag(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
}
|
||||
|
||||
digest := v1.Hash{
|
||||
Algorithm: "sha256",
|
||||
Hex: "digest",
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
replaced, err := oci.ReplaceTag(c.name, digest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expected, replaced)
|
||||
})
|
||||
}
|
||||
}
|
||||
144
oci/output.go
Normal file
144
oci/output.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
// PushImageToRegistry pushes an image to the registry with the specified name.
|
||||
func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err)
|
||||
}
|
||||
|
||||
// Push the image to the registry
|
||||
return remote.Write(ref, image, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// PushIndexToRegistry pushes an index to the registry with the specified name.
|
||||
func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
|
||||
// Parse the index name
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse image name: %w", err)
|
||||
}
|
||||
|
||||
// Push the index to the registry
|
||||
return remote.WriteIndex(ref, index, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an image as an OCI layout to the specified path.
|
||||
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||
// Save the image to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
index := empty.Index
|
||||
l, err := layout.Write(path, index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
return l.AppendImage(image)
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path.
|
||||
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
||||
// Save the index to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
_, err = layout.Write(path, image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveIndex saves an index to the specified outputs.
|
||||
func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error {
|
||||
// split output by comma and write or push each one
|
||||
for _, output := range outputs {
|
||||
if output.Type == OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: index,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
OCIReferenceTarget: indexName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushIndexToRegistry(index, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImage saves an image to the specified output.
|
||||
func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
|
||||
if output.Type == OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: image,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
OCIReferenceTarget: imageName,
|
||||
},
|
||||
},
|
||||
})
|
||||
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write signed image: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := PushImageToRegistry(image, output.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push signed image: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImagesNoTag saves a list of images by digest to the specified outputs.
|
||||
func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error {
|
||||
for _, output := range outputs {
|
||||
// OCI layout output not supported
|
||||
if output.Type == OCI {
|
||||
continue
|
||||
}
|
||||
for _, image := range images {
|
||||
digest, err := image.Digest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get image digest: %w", err)
|
||||
}
|
||||
spec, err := ReplaceDigestInSpec(output, digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create image spec: %w", err)
|
||||
}
|
||||
err = PushImageToRegistry(image, spec.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
109
oci/output_test.go
Normal file
109
oci/output_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/attestation"
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/oci"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSavingIndex(t *testing.T) {
|
||||
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage(".."))
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(registry.New())
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex(output, attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ociOutput, err := oci.ParseImageSpecs(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSavingImage(t *testing.T) {
|
||||
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||
|
||||
img := empty.Image
|
||||
|
||||
server := httptest.NewServer(registry.New())
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpec(indexName)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImage(output, img, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ociOutput, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImage(ociOutput, img, indexName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSavingReferrers(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
opts := &attestation.SigningOptions{}
|
||||
statement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
}
|
||||
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
require.NoError(t, err)
|
||||
subject := &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := attestation.NewManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.Add(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &attestation.MockRegistryResolver{
|
||||
Subject: subject,
|
||||
MockResolver: &attestation.MockResolver{},
|
||||
ImageNameStr: indexName,
|
||||
}
|
||||
require.NoError(t, err)
|
||||
refResolver, err := attestation.NewReferrersResolver(reg)
|
||||
require.NoError(t, err)
|
||||
attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, attestations, 1)
|
||||
}
|
||||
61
oci/registry.go
Normal file
61
oci/registry.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type RegistryImageDetailsResolver struct {
|
||||
*ImageSpec
|
||||
descriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
|
||||
return &RegistryImageDetailsResolver{
|
||||
ImageSpec: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.Identifier, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return r.Platform, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||
if r.descriptor == nil {
|
||||
subjectRef, err := name.ParseReference(r.Identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
options := WithOptions(ctx, r.Platform)
|
||||
image, err := remote.Image(subjectRef, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image manifest: %w", err)
|
||||
}
|
||||
digest, err := image.Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image digest: %w", err)
|
||||
}
|
||||
size, err := image.Size()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image size: %w", err)
|
||||
}
|
||||
mediaType, err := image.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image media type: %w", err)
|
||||
}
|
||||
r.descriptor = &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: size,
|
||||
MediaType: mediaType,
|
||||
}
|
||||
}
|
||||
return r.descriptor, nil
|
||||
}
|
||||
13
oci/resolver.go
Normal file
13
oci/resolver.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type ImageDetailsResolver interface {
|
||||
ImageName(ctx context.Context) (string, error)
|
||||
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||
ImageDescriptor(ctx context.Context) (*v1.Descriptor, error)
|
||||
}
|
||||
217
oci/types.go
Normal file
217
oci/types.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
const (
|
||||
OCIReferenceTarget = "org.opencontainers.image.ref.name"
|
||||
LocalPrefix = "oci://"
|
||||
RegistryPrefix = "docker://"
|
||||
OCI SourceType = "OCI"
|
||||
Docker SourceType = "Docker"
|
||||
)
|
||||
|
||||
type (
|
||||
SourceType string
|
||||
NamedIndex struct {
|
||||
Index v1.ImageIndex
|
||||
Name string
|
||||
}
|
||||
)
|
||||
|
||||
type ImageSpecOption func(*ImageSpec) error
|
||||
|
||||
type ImageSpec struct {
|
||||
// OCI or Docker
|
||||
Type SourceType
|
||||
// without oci:// or docker:// (name or path)
|
||||
Identifier string
|
||||
Platform *v1.Platform
|
||||
}
|
||||
|
||||
func IndexFromPath(path string) (*NamedIndex, error) {
|
||||
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := wrapperIdx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
imageName := idxm.Manifests[0].Annotations[OCIReferenceTarget]
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
idx, err := wrapperIdx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return &NamedIndex{
|
||||
Index: idx,
|
||||
Name: imageName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func IndexFromRemote(image string) (*NamedIndex, error) {
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
|
||||
}
|
||||
|
||||
// Pull the image from the registry
|
||||
idx, err := remote.Index(ref, MultiKeychainOption())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
|
||||
}
|
||||
return &NamedIndex{
|
||||
Index: idx,
|
||||
Name: image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func LoadIndex(input *ImageSpec) (*NamedIndex, error) {
|
||||
if input.Type == OCI {
|
||||
return IndexFromPath(input.Identifier)
|
||||
}
|
||||
return IndexFromRemote(input.Identifier)
|
||||
}
|
||||
|
||||
func (i *ImageSpec) ForPlatforms(platform string) ([]*ImageSpec, error) {
|
||||
platforms := strings.Split(platform, ",")
|
||||
var specs []*ImageSpec
|
||||
for _, pStr := range platforms {
|
||||
p, err := ParsePlatform(pStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := &ImageSpec{
|
||||
Type: i.Type,
|
||||
Identifier: i.Identifier,
|
||||
Platform: p,
|
||||
}
|
||||
specs = append(specs, spec)
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func ParseImageSpec(img string, options ...ImageSpecOption) (*ImageSpec, error) {
|
||||
img = strings.TrimSpace(img)
|
||||
if strings.Contains(img, ",") {
|
||||
return nil, fmt.Errorf("only one image is supported")
|
||||
}
|
||||
withoutPrefix := strings.TrimPrefix(strings.TrimPrefix(img, LocalPrefix), RegistryPrefix)
|
||||
src := &ImageSpec{
|
||||
Identifier: withoutPrefix,
|
||||
}
|
||||
if strings.HasPrefix(img, LocalPrefix) {
|
||||
src.Type = OCI
|
||||
} else {
|
||||
src.Type = Docker
|
||||
}
|
||||
for _, option := range options {
|
||||
err := option(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if src.Platform == nil {
|
||||
platform, err := ParsePlatform("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
src.Platform = platform
|
||||
}
|
||||
return src, nil
|
||||
}
|
||||
|
||||
func WithPlatform(platform string) ImageSpecOption {
|
||||
return func(i *ImageSpec) error {
|
||||
if strings.Contains(platform, ",") {
|
||||
return fmt.Errorf("only one platform is supported")
|
||||
}
|
||||
p, err := ParsePlatform(platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Platform = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParseImageSpecs(img string) ([]*ImageSpec, error) {
|
||||
outputs := strings.Split(img, ",")
|
||||
var sources []*ImageSpec
|
||||
for _, output := range outputs {
|
||||
src, err := ParseImageSpec(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources = append(sources, src)
|
||||
}
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
func WithoutTag(image string) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
prefix := ""
|
||||
if strings.HasPrefix(image, RegistryPrefix) {
|
||||
image = strings.TrimPrefix(image, RegistryPrefix)
|
||||
prefix = RegistryPrefix
|
||||
}
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
repo := ref.Context().Name()
|
||||
return prefix + repo, nil
|
||||
}
|
||||
|
||||
type EmptyConfigImage struct {
|
||||
v1.Image
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
|
||||
mf, err := i.Image.Manifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
}
|
||||
mf.Config = v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.empty.v1+json",
|
||||
Size: 2,
|
||||
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
||||
Data: []byte("{}"),
|
||||
}
|
||||
return mf, nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
||||
mf, err := i.Manifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
}
|
||||
return json.Marshal(mf)
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
|
||||
mb, err := i.RawManifest()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
||||
return digest, err
|
||||
}
|
||||
21
oci/types_test.go
Normal file
21
oci/types_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmptyConfigImageDigest(t *testing.T) {
|
||||
empty := empty.Image
|
||||
img := EmptyConfigImage{Image: empty}
|
||||
mf, err := img.RawManifest()
|
||||
require.NoError(t, err)
|
||||
hash := util.SHA256Hex(mf)
|
||||
digest, err := img.Digest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, digest.Hex, hash)
|
||||
}
|
||||
Reference in New Issue
Block a user