Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/service-image-plumbing.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,18 @@ The credential boundary and repository dispatch restrictions are described in
`cmd/image-sbom` stays in the services: it chooses service images, platforms,
output paths, and CLI behavior using these APIs. Adoption follows the core
release by pinning it and removing service-local copies.


## Image presence at run time

Before a local container run pulls an image, `runners/dockerrun.ImageExists`
asks the daemon to resolve the **exact reference** the run names — `name:tag`
or `name@sha256:…` — with `ImageInspect`, the same resolution the later
container create performs. A digest reference resolves only under the
repository name the image carries it as: the same bytes under another name,
or an image whose ID merely equals the digest, are not that reference, and a
match on them would report present an image the create then cannot find. A
reference the daemon cannot resolve is an answer (`false`, no error) and the
pull follows; any other daemon failure is an error, so "cannot tell" never
becomes "pull it". An image built or imported locally and never pushed is
present under its own `name@sha256:<ID>` and under its tag, and is not pulled.
29 changes: 18 additions & 11 deletions runners/dockerrun/docker_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2060,19 +2060,26 @@ func (docker *DockerEnvironment) ImageExists(ctx context.Context, imag *resource

}

// ImageExists reports whether the daemon can resolve the exact reference imag
// names — a tag (name:tag) or a digest (name@sha256:…) — the way the later
// container create will have to. It asks the daemon rather than reading the
// image list: a digest reference resolves only under the repository name the
// image carries it as, so the same bytes under another name, or an image whose
// ID merely equals the digest, are NOT this reference, and a scan that matched
// them reported an image present that the create then could not find. Before
// this it compared RepoTags alone, which read every digest reference — the form
// a local build hands back — as absent and sent the caller to pull it.
//
// Not found is an answer, false with no error; any other daemon failure is an
// error, because "cannot tell" must not become "pull it".
func ImageExists(ctx context.Context, c *client.Client, imag *resources.DockerImage) (bool, error) {
w := wool.Get(ctx).In("Docker.ImageExists")
images, err := c.ImageList(ctx, image.ListOptions{})
if err != nil {
return false, w.Wrapf(err, "cannot list images")
}
for i := range images {
img := &images[i]
for _, repoTag := range img.RepoTags {
if repoTag == imag.FullName() {
return true, nil
}
reference := imag.FullName()
if _, err := c.ImageInspect(ctx, reference); err != nil {
if client.IsErrNotFound(err) {
return false, nil
}
return false, w.Wrapf(err, "cannot inspect image %s", reference)
}
return false, nil
return true, nil
}
83 changes: 83 additions & 0 deletions runners/dockerrun/image_exists_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build !skip_infra

package dockerrun_test

import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
"strings"
"testing"
"time"

"github.com/codefly-dev/core/resources"
"github.com/codefly-dev/core/runners/dockerrun"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"github.com/stretchr/testify/require"
)

// TestImageExistsResolvesTheExactReference: an image imported here, never
// pulled, is present under its tag and under its own name@digest, which is the
// form a local build hands back; the same digest requested under another
// repository name is a reference the daemon cannot resolve and is reported
// absent, not present; and a daemon that cannot be reached is an error, not an
// absence.
func TestImageExistsResolvesTheExactReference(t *testing.T) {
requireDocker(t)
ctx := context.Background()
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
require.NoError(t, err)
defer c.Close()

// Imported from a tarball, so nothing is pulled: the image exists only here.
var tarball bytes.Buffer
tw := tar.NewWriter(&tarball)
content := []byte("codefly image-exists test " + time.Now().String())
require.NoError(t, tw.WriteHeader(&tar.Header{Name: "marker", Mode: 0o600, Size: int64(len(content))}))
_, err = tw.Write(content)
require.NoError(t, err)
require.NoError(t, tw.Close())
name := fmt.Sprintf("codefly-test/image-exists-%d", time.Now().UnixNano())
tag := "local"
body, err := c.ImageImport(ctx, image.ImportSource{Source: &tarball, SourceName: "-"}, name+":"+tag, image.ImportOptions{})
require.NoError(t, err)
_, _ = io.Copy(io.Discard, body)
_ = body.Close()
inspected, err := c.ImageInspect(ctx, name+":"+tag)
require.NoError(t, err)
defer func() { _, _ = c.ImageRemove(ctx, inspected.ID, image.RemoveOptions{Force: true}) }()
digest := inspected.ID
require.True(t, strings.HasPrefix(digest, "sha256:"))

byTag := &resources.DockerImage{Name: name, Tag: tag}
exists, err := dockerrun.ImageExists(ctx, c, byTag)
require.NoError(t, err)
require.True(t, exists, "present under its tag")

byDigest := &resources.DockerImage{Name: name, Tag: tag, Digest: digest}
exists, err = dockerrun.ImageExists(ctx, c, byDigest)
require.NoError(t, err)
require.True(t, exists, "present under its own name@digest, the form a local build hands back")

otherRepository := &resources.DockerImage{Repository: "example.invalid", Name: "review-no-such-image", Digest: digest}
exists, err = dockerrun.ImageExists(ctx, c, otherRepository)
require.NoError(t, err, "a reference the daemon cannot resolve is an answer, not a failure")
require.False(t, exists, "the same digest under another repository name is not a reference this daemon resolves")

otherTag := &resources.DockerImage{Name: name, Tag: "never-tagged"}
exists, err = dockerrun.ImageExists(ctx, c, otherTag)
require.NoError(t, err)
require.False(t, exists)

unreachable, err := client.NewClientWithOpts(client.WithHost("tcp://127.0.0.1:1"), client.WithVersion(c.ClientVersion()))
require.NoError(t, err)
defer unreachable.Close()
deadline, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_, err = dockerrun.ImageExists(deadline, unreachable, byTag)
require.Error(t, err, "a daemon that cannot answer is an error, never an absence that leads to a pull")
require.ErrorContains(t, err, "cannot inspect image")
}
Loading