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
91 changes: 79 additions & 12 deletions pkg/images/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Load(ctx context.Context, lr *cluster.LookupResult, archive io.Reader, logg
before := listRefSet(ctx, lr)

args := []string{"-n", "k8s.io", "image", "import", "-"}
logger.Info("loading image archive",
logger.Debug("loading image archive",
zap.String("backend", string(lr.Backend)),
zap.String("cluster", lr.ClusterName),
)
Expand All @@ -74,7 +74,7 @@ func Load(ctx context.Context, lr *cluster.LookupResult, archive io.Reader, logg
stdout.String(), stderr.String(), err)
}
if stdout.Len() > 0 {
logger.Info("ctr image import",
logger.Debug("ctr image import",
zap.String("output", stdout.String()),
)
}
Expand All @@ -90,16 +90,15 @@ func Load(ctx context.Context, lr *cluster.LookupResult, archive io.Reader, logg
logger.Warn("post-import snapshot failed; skipping digest-alias step")
return nil
}
aliased := 0
created := map[string]string{}
for _, p := range after {
if before[p.ref] {
continue // existed before this import
}
if strings.Contains(p.ref, "@") {
continue // already digest-form; nothing to alias
alias := aliasFor(p.ref, p.digest)
if alias == "" {
continue
}
nameOnly := stripTag(p.ref)
alias := nameOnly + "@" + p.digest
if before[alias] {
continue // alias already exists somehow; skip
}
Expand All @@ -109,24 +108,92 @@ func Load(ctx context.Context, lr *cluster.LookupResult, archive io.Reader, logg
// Don't fail the whole Load -- the import already
// landed. Log so the operator sees what didn't
// alias and can do it manually if needed.
logger.Warn("ctr image tag (digest alias) failed",
logger.Warn("ctr image tag (alias) failed",
zap.String("ref", p.ref),
zap.String("alias", alias),
zap.String("stderr", tagErr.String()),
zap.Error(err))
continue
}
aliased++
logger.Info("digest alias created",
created[p.ref] = alias
logger.Debug("alias created",
zap.String("ref", p.ref),
zap.String("alias", alias))
}
if aliased == 0 {
logger.Info("no new digest aliases needed (re-import or already aliased)")

// Happy-path summary: one INFO per new ref that names a real
// repo. ctr writes a bare config-digest row ("sha256:<hex>")
// alongside the canonical ref when importing; we filter it
// here so the operator only sees lines for things they pinned.
any := false
for _, p := range after {
if before[p.ref] {
continue
}
if strings.HasPrefix(p.ref, "sha256:") {
continue
}
any = true
var line string
if strings.Contains(p.ref, "@") {
// Digest-form ref already carries the digest; don't
// repeat it in the log line.
line = "imported " + p.ref
} else {
short := p.digest
if strings.HasPrefix(short, "sha256:") && len(short) > 19 {
short = short[:19]
}
line = fmt.Sprintf("imported %s @%s", p.ref, short)
}
if _, ok := created[p.ref]; ok {
if strings.Contains(p.ref, "@") {
line += " (+1 :latest alias)"
} else {
line += " (+1 digest alias)"
}
}
logger.Info(line)
}
if !any {
logger.Info("no new image refs (already loaded)")
}
return nil
}

// aliasFor decides what alias (if any) to create for a new
// post-import ref. Two cases produce a useful alias; one (the
// bare config-digest row containerd writes alongside the
// canonical ref) produces "" so the caller skips.
//
// - Tag-form ref ("<repo>:<tag>") -> "<repo>@<digest>".
// Required by deployments pinning images in name:tag@sha256:
// digest form; without it kubelet's digest lookup misses the
// tag-only row in containerd's image store and falls back to
// a registry pull.
//
// - Digest-form ref ("<repo>@<digest>") -> "<repo>:latest@
// <digest>". Required by kubelet's checkpoint-image check on
// containerd v2: it resolves images by config-digest and
// normalizes the bare config-digest row to "docker.io/library/
// sha256@..." (not found). A tag-form alias gives the lookup
// a parseable repo to land on.
//
// - Bare "sha256:<hex>" (the config-digest row ctr emits as a
// side effect) -> "". Treating it as a tagged ref would
// mangle it through stripTag into the literal "sha256" and
// synthesize "sha256@sha256:..." -- a garbage entry that
// poisons the image store.
func aliasFor(ref, digest string) string {
if strings.HasPrefix(ref, "sha256:") {
return ""
}
if at := strings.Index(ref, "@"); at >= 0 {
return ref[:at] + ":latest" + ref[at:]
}
return stripTag(ref) + "@" + digest
}

// TarOCIDir streams a USTAR archive of an OCI v1 layout rooted
// at dir into w. The entries are dir-relative (oci-layout,
// index.json, blobs/sha256/*) -- the same shape `tar -cf - -C
Expand Down
40 changes: 40 additions & 0 deletions pkg/images/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,43 @@ func TestStripTag(t *testing.T) {
}
}
}

// TestAliasFor pins the post-import alias policy. Each case
// captures one of the three shapes ctr writes into the image
// store after `image import`: tag-form, digest-form, and the
// bare config-digest row that mustn't be aliased.
func TestAliasFor(t *testing.T) {
const digest = "sha256:af91c49ce795f3b2c1a4e6d8b9c0e1f2a3b4c5d6e7f80112233445566778899aa"
cases := []struct {
name, ref, want string
}{
{
name: "tag-form -> digest alias",
ref: "ghcr.io/yolean/echo:v1",
want: "ghcr.io/yolean/echo@" + digest,
},
{
name: "digest-form -> :latest alias (kubelet checkpoint-image lookup)",
ref: "ghcr.io/yolean/minio-deduplication@" + digest,
want: "ghcr.io/yolean/minio-deduplication:latest@" + digest,
},
{
name: "bare config-digest row -> no alias (would mangle to sha256@sha256:...)",
ref: "sha256:dc863b8391abb7c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70819253647586978a9",
want: "",
},
{
name: "hostport tag stripped at correct colon",
ref: "registry.example:5000/foo/bar:tag",
want: "registry.example:5000/foo/bar@" + digest,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := aliasFor(c.ref, digest)
if got != c.want {
t.Errorf("aliasFor(%q) = %q, want %q", c.ref, got, c.want)
}
})
}
}