Skip to content

Commit 842e29a

Browse files
authored
Merge pull request #2579 from carolynvs/build-ref-digest-perf
Do not pull referenced images during build
2 parents 28ee71e + 74e196e commit 842e29a

File tree

12 files changed

+211
-95
lines changed

12 files changed

+211
-95
lines changed

cmd/porter/bundle.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ The docker driver builds the bundle image using the local Docker host. To use a
9191
"Do not use the Docker cache when building the bundle's invocation image.")
9292
f.StringArrayVar(&opts.Customs, "custom", nil,
9393
"Define an individual key-value pair for the custom section in the form of NAME=VALUE. Use dot notation to specify a nested custom field. May be specified multiple times. Max length is 5,000 characters when used as a build argument.")
94+
f.BoolVar(&opts.InsecureRegistry, "insecure-registry", false,
95+
"Don't require TLS when pulling referenced images")
9496

9597
// Allow configuring the --driver flag with build-driver, to avoid conflicts with other commands
9698
cmd.Flag("driver").Annotations = map[string][]string{

docs/content/cli/build.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ porter build [flags]
4242
-d, --dir string Path to the build context directory where all bundle assets are located. Defaults to the current directory.
4343
-f, --file string Path to the Porter manifest. The path is relative to the build context directory. Defaults to porter.yaml in the current directory.
4444
-h, --help help for build
45+
--insecure-registry Don't require TLS when pulling referenced images
4546
--name string Override the bundle name
4647
--no-cache Do not use the Docker cache when building the bundle's invocation image.
4748
--no-lint Do not run the linter

docs/content/cli/bundles_build.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ porter bundles build [flags]
4242
-d, --dir string Path to the build context directory where all bundle assets are located. Defaults to the current directory.
4343
-f, --file string Path to the Porter manifest. The path is relative to the build context directory. Defaults to porter.yaml in the current directory.
4444
-h, --help help for build
45+
--insecure-registry Don't require TLS when pulling referenced images
4546
--name string Override the bundle name
4647
--no-cache Do not use the Docker cache when building the bundle's invocation image.
4748
--no-lint Do not run the linter

pkg/cnab/cnab-to-oci/helpers.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,25 @@ type TestRegistry struct {
1515
MockPullBundle func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (cnab.BundleReference, error)
1616
MockPushBundle func(ctx context.Context, ref cnab.BundleReference, opts RegistryOptions) (bundleReference cnab.BundleReference, err error)
1717
MockPushImage func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (imageDigest digest.Digest, err error)
18-
MockGetCachedImage func(ctx context.Context, ref cnab.OCIReference) (ImageSummary, error)
18+
MockGetCachedImage func(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error)
1919
MockListTags func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) ([]string, error)
2020
MockPullImage func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) error
2121
MockGetBundleMetadata func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (BundleMetadata, error)
22-
cache map[string]ImageSummary
22+
MockGetImageMetadata func(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error)
23+
cache map[string]ImageMetadata
2324
}
2425

2526
func NewTestRegistry() *TestRegistry {
2627
return &TestRegistry{
27-
cache: make(map[string]ImageSummary),
28+
cache: make(map[string]ImageMetadata),
2829
}
2930
}
3031

3132
func (t TestRegistry) PullBundle(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (cnab.BundleReference, error) {
3233
if t.MockPullBundle != nil {
3334
return t.MockPullBundle(ctx, ref, opts)
3435
}
35-
sum, _ := NewImageSummary(ref.String(), types.ImageInspect{ID: cnab.NewULID()})
36+
sum, _ := NewImageSummaryFromInspect(ref, types.ImageInspect{ID: cnab.NewULID()})
3637
t.cache[ref.String()] = sum
3738

3839
return cnab.BundleReference{Reference: ref}, nil
@@ -53,19 +54,35 @@ func (t *TestRegistry) PushImage(ctx context.Context, ref cnab.OCIReference, opt
5354
return "sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041", nil
5455
}
5556

56-
func (t *TestRegistry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageSummary, error) {
57+
func (t *TestRegistry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error) {
5758
if t.MockGetCachedImage != nil {
5859
return t.MockGetCachedImage(ctx, ref)
5960
}
6061

6162
img := ref.String()
6263
sum, ok := t.cache[img]
6364
if !ok {
64-
return ImageSummary{}, fmt.Errorf("image %s not found in cache", img)
65+
return ImageMetadata{}, fmt.Errorf("failed to find image in docker cache: %w", ErrNotFound{Reference: ref})
6566
}
6667
return sum, nil
6768
}
6869

70+
func (t TestRegistry) GetImageMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error) {
71+
meta, err := t.GetCachedImage(ctx, ref)
72+
if err != nil {
73+
if t.MockGetImageMetadata != nil {
74+
return t.MockGetImageMetadata(ctx, ref, opts)
75+
} else {
76+
mockImg := ImageMetadata{
77+
Reference: ref,
78+
RepoDigests: []string{fmt.Sprintf("%s@sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041", ref.Repository())},
79+
}
80+
return mockImg, nil
81+
}
82+
}
83+
return meta, nil
84+
}
85+
6986
func (t *TestRegistry) ListTags(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) ([]string, error) {
7087
if t.MockListTags != nil {
7188
return t.MockListTags(ctx, ref, opts)
@@ -80,7 +97,7 @@ func (t *TestRegistry) PullImage(ctx context.Context, ref cnab.OCIReference, opt
8097
}
8198

8299
image := ref.String()
83-
sum, err := NewImageSummary(image, types.ImageInspect{
100+
sum, err := NewImageSummaryFromInspect(ref, types.ImageInspect{
84101
ID: cnab.NewULID(),
85102
RepoDigests: []string{fmt.Sprintf("%s@sha256:75c495e5ce9c428d482973d72e3ce9925e1db304a97946c9aa0b540d7537e041", image)},
86103
})

pkg/cnab/cnab-to-oci/provider.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ type RegistryProvider interface {
2323

2424
// GetCachedImage returns a particular image from the local image cache.
2525
// Use ErrNotFound to detect if the failure is because the image is not in the local Docker cache.
26-
GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageSummary, error)
26+
GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error)
2727

2828
// ListTags returns all tags defined on the specified repository.
2929
ListTags(ctx context.Context, repo cnab.OCIReference, opts RegistryOptions) ([]string, error)
3030

3131
// PullImage pulls an image from an OCI registry and returns the image's digest
3232
PullImage(ctx context.Context, image cnab.OCIReference, opts RegistryOptions) error
3333

34+
// GetImageMetadata returns information about an image in a registry
35+
// Use ErrNotFound to detect if the error is because the image is not in the registry.
36+
GetImageMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error)
37+
3438
// GetBundleMetadata returns information about a bundle in a registry
3539
// Use ErrNotFound to detect if the error is because the bundle is not in the registry.
3640
GetBundleMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (BundleMetadata, error)

pkg/cnab/cnab-to-oci/registry.go

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -266,24 +266,27 @@ func (r *Registry) displayEvent(ev remotes.FixupEvent) {
266266
}
267267

268268
// GetCachedImage returns information about an image from local docker cache.
269-
func (r *Registry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageSummary, error) {
269+
func (r *Registry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error) {
270270
image := ref.String()
271271
ctx, log := tracing.StartSpan(ctx, attribute.String("reference", image))
272272
defer log.EndSpan()
273273

274274
cli, err := docker.GetDockerClient()
275275
if err != nil {
276-
return ImageSummary{}, log.Error(err)
276+
return ImageMetadata{}, log.Error(err)
277277
}
278278

279279
result, _, err := cli.Client().ImageInspectWithRaw(ctx, image)
280280
if err != nil {
281-
return ImageSummary{}, log.Error(fmt.Errorf("failed to find image in docker cache: %w", ErrNotFound{Reference: ref}))
281+
err = fmt.Errorf("failed to find image in docker cache: %w", ErrNotFound{Reference: ref})
282+
// log as debug because this isn't a terminal error
283+
log.Debugf(err.Error())
284+
return ImageMetadata{}, err
282285
}
283286

284-
summary, err := NewImageSummary(image, result)
287+
summary, err := NewImageSummaryFromInspect(ref, result)
285288
if err != nil {
286-
return ImageSummary{}, log.Error(fmt.Errorf("failed to extract image %s in docker cache: %w", image, err))
289+
return ImageMetadata{}, log.Error(fmt.Errorf("failed to extract image %s in docker cache: %w", image, err))
287290
}
288291

289292
return summary, nil
@@ -331,6 +334,41 @@ func (r *Registry) GetBundleMetadata(ctx context.Context, ref cnab.OCIReference,
331334
}, nil
332335
}
333336

337+
// GetImageMetadata returns information about an image in a registry
338+
// Use ErrNotFound to detect if the error is because the image is not in the registry.
339+
func (r *Registry) GetImageMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error) {
340+
ctx, span := tracing.StartSpan(ctx, attribute.String("reference", ref.String()))
341+
defer span.EndSpan()
342+
343+
// Check if we already have the image in the Docker cache
344+
cachedResult, err := r.GetCachedImage(ctx, ref)
345+
if err != nil {
346+
if !errors.Is(err, ErrNotFound{}) {
347+
return ImageMetadata{}, err
348+
}
349+
}
350+
351+
// Check if we have the repository digest cached for the referenced image
352+
if cachedDigest, err := cachedResult.GetRepositoryDigest(); err == nil {
353+
span.SetAttributes(attribute.String("cached-digest", cachedDigest.String()))
354+
return cachedResult, nil
355+
}
356+
357+
// Do a HEAD against the registry to retrieve image metadata without pulling the entire image contents
358+
desc, err := crane.Head(ref.String(), opts.toCraneOptions()...)
359+
if err != nil {
360+
if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil {
361+
return ImageMetadata{}, span.Error(notFoundErr)
362+
}
363+
return ImageMetadata{}, span.Errorf("error fetching image metadata for %s: %w", ref, err)
364+
}
365+
366+
repoDigest := digest.NewDigestFromHex(desc.Digest.Algorithm, desc.Digest.Hex)
367+
span.SetAttributes(attribute.String("fetched-digest", repoDigest.String()))
368+
369+
return NewImageSummaryFromDigest(ref, repoDigest)
370+
}
371+
334372
// asNotFoundError checks if the error is an HTTP 404 not found error, and if so returns a corresponding ErrNotFound instance.
335373
func asNotFoundError(err error, ref cnab.OCIReference) error {
336374
var httpError *transport.Error
@@ -343,49 +381,57 @@ func asNotFoundError(err error, ref cnab.OCIReference) error {
343381
return nil
344382
}
345383

346-
// ImageSummary contains information about an OCI image.
347-
type ImageSummary struct {
348-
types.ImageInspect
349-
imageRef cnab.OCIReference
384+
// ImageMetadata contains information about an OCI image.
385+
type ImageMetadata struct {
386+
Reference cnab.OCIReference
387+
RepoDigests []string
350388
}
351389

352-
func NewImageSummary(imageRef string, sum types.ImageInspect) (ImageSummary, error) {
353-
ref, err := cnab.ParseOCIReference(imageRef)
354-
if err != nil {
355-
return ImageSummary{}, err
356-
}
357-
358-
img := ImageSummary{
359-
imageRef: ref,
360-
ImageInspect: sum,
390+
func NewImageSummaryFromInspect(ref cnab.OCIReference, sum types.ImageInspect) (ImageMetadata, error) {
391+
img := ImageMetadata{
392+
Reference: ref,
393+
RepoDigests: sum.RepoDigests,
361394
}
362395
if img.IsZero() {
363-
return ImageSummary{}, fmt.Errorf("invalid image summary for image reference %s", imageRef)
396+
return ImageMetadata{}, fmt.Errorf("invalid image summary for image reference %s", ref)
364397
}
365398

366399
return img, nil
367400
}
368401

369-
func (i ImageSummary) GetImageReference() cnab.OCIReference {
370-
return i.imageRef
402+
func NewImageSummaryFromDigest(ref cnab.OCIReference, repoDigest digest.Digest) (ImageMetadata, error) {
403+
digestedRef, err := ref.WithDigest(repoDigest)
404+
if err != nil {
405+
return ImageMetadata{}, fmt.Errorf("error building an OCI reference from image %s and digest %s", ref.Repository(), ref.Digest())
406+
}
407+
408+
return ImageMetadata{
409+
Reference: ref,
410+
RepoDigests: []string{digestedRef.String()},
411+
}, nil
412+
}
413+
414+
func (i ImageMetadata) String() string {
415+
return i.Reference.String()
371416
}
372417

373-
func (i ImageSummary) IsZero() bool {
374-
return i.ID == ""
418+
func (i ImageMetadata) IsZero() bool {
419+
return i.String() == ""
375420
}
376421

377-
// Digest returns the image digest for the image reference.
378-
func (i ImageSummary) Digest() (digest.Digest, error) {
422+
// GetRepositoryDigest finds the repository digest associated with the original
423+
// image reference used to create this ImageMetadata.
424+
func (i ImageMetadata) GetRepositoryDigest() (digest.Digest, error) {
379425
if len(i.RepoDigests) == 0 {
380-
return "", fmt.Errorf("failed to get digest for image: %s", i.imageRef.String())
426+
return "", fmt.Errorf("failed to get digest for image: %s", i)
381427
}
382428
var imgDigest digest.Digest
383429
for _, rd := range i.RepoDigests {
384430
imgRef, err := cnab.ParseOCIReference(rd)
385431
if err != nil {
386432
return "", err
387433
}
388-
if imgRef.Repository() != i.imageRef.Repository() {
434+
if imgRef.Repository() != i.Reference.Repository() {
389435
continue
390436
}
391437

@@ -398,7 +444,7 @@ func (i ImageSummary) Digest() (digest.Digest, error) {
398444
}
399445

400446
if imgDigest == "" {
401-
return "", fmt.Errorf("cannot find image digest for desired repo %s", i.imageRef.String())
447+
return "", fmt.Errorf("cannot find image digest for desired repo %s", i)
402448
}
403449

404450
if err := imgDigest.Validate(); err != nil {

pkg/cnab/cnab-to-oci/registry_test.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"get.porter.sh/porter/pkg/cnab"
88
"github.com/docker/docker/api/types"
99
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
10+
"github.com/opencontainers/go-digest"
11+
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113
)
1214

@@ -31,13 +33,6 @@ func TestImageSummary(t *testing.T) {
3133
expected: expectedOutput{imageRef: "test/image:latest", digest: "sha256:6b5a28ccbb76f12ce771a23757880c6083234255c5ba191fca1c5db1f71c1687"},
3234
expectedErr: "",
3335
},
34-
{
35-
name: "invalid image reference",
36-
imgRef: "test-",
37-
imageSummary: types.ImageInspect{ID: "test", RepoDigests: []string{"test/image@sha256:6b5a28ccbb76f12ce771a23757880c6083234255c5ba191fca1c5db1f71c1687"}},
38-
expected: expectedOutput{hasInitErr: true},
39-
expectedErr: "invalid reference format",
40-
},
4136
{
4237
name: "empty repo digests",
4338
imgRef: "test/image:latest",
@@ -61,13 +56,13 @@ func TestImageSummary(t *testing.T) {
6156
for _, tt := range testcases {
6257
tt := tt
6358
t.Run(tt.name, func(t *testing.T) {
64-
sum, err := NewImageSummary(tt.imgRef, tt.imageSummary)
59+
sum, err := NewImageSummaryFromInspect(cnab.MustParseOCIReference(tt.imgRef), tt.imageSummary)
6560
if tt.expected.hasInitErr {
6661
require.ErrorContains(t, err, tt.expectedErr)
6762
return
6863
}
69-
require.Equal(t, sum.GetImageReference().String(), tt.expected.imageRef)
70-
digest, err := sum.Digest()
64+
require.Equal(t, sum.Reference.String(), tt.expected.imageRef)
65+
digest, err := sum.GetRepositoryDigest()
7166
if tt.expected.digest == "" {
7267
require.ErrorContains(t, err, tt.expectedErr)
7368
return
@@ -77,6 +72,19 @@ func TestImageSummary(t *testing.T) {
7772
}
7873
}
7974

75+
func TestNewImageSummaryFromDescriptor(t *testing.T) {
76+
ref := cnab.MustParseOCIReference("localhost:5000/whalesayd:latest")
77+
origRepoDigest := digest.Digest("sha256:499f71eec2e3bd78f26c268bbf5b2a65f73b96216fac4a89b86b5ebf115527b6")
78+
79+
s, err := NewImageSummaryFromDigest(ref, origRepoDigest)
80+
require.NoError(t, err, "NewImageSummaryFromDigest failed")
81+
82+
// Locate the repository digest associated with the reference and validate that it matches what we input
83+
repoDigest, err := s.GetRepositoryDigest()
84+
require.NoError(t, err, "failed to get repository digest for image summary")
85+
assert.Equal(t, origRepoDigest.String(), repoDigest.String())
86+
}
87+
8088
func TestAsNotFoundError(t *testing.T) {
8189
ref := cnab.MustParseOCIReference("example.com/mybuns:v1.2.3")
8290
t.Run("404", func(t *testing.T) {

pkg/porter/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type BuildOptions struct {
3535
// Custom is the unparsed list of NAME=VALUE custom inputs set on the command line.
3636
Customs []string
3737

38+
// InsecureRegistry allows connecting to an unsecured registry or one without verifiable certificates.
39+
InsecureRegistry bool
40+
3841
// parsedCustoms is the parsed set of custom inputs from Customs.
3942
parsedCustoms map[string]string
4043
}

0 commit comments

Comments
 (0)