feat(imagepush): 写出按架构的发布镜像包并支持按名推送

This commit is contained in:
Lemon-miaow committed 2026-09-26 14:12:26 +08:00
1 parent c24396d50a
commit 7beb43195b
8 files changed
+1135 -3

No files matched your search

+184
View File
@@ -0,0 +1,184 @@
package imagepush
import (
"context"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
)
// An OCI image layout tar is what a release ships its images in: oci-layout,
// blobs/sha256/<hex> and an index.json whose entries each name one image through
// the io.containerd.image.name annotation. `ctr images import` reads that
// annotation, so a host imports the images straight into k3s's containerd under
// the names the pods use, and PushLayout pushes the same bytes into the platform
// registry: the manifest goes up verbatim, so the registry and containerd agree on
// every digest.
const (
annotationImageName = "io.containerd.image.name"
annotationRefName = "org.opencontainers.image.ref.name"
layoutIndexFile = "index.json"
layoutMarkerFile = "oci-layout"
layoutMarkerContent = `{"imageLayoutVersion":"1.0.0"}`
)
// layoutDescriptor is one entry of an index: index.json's or a nested index's.
type layoutDescriptor struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
Annotations map[string]string `json:"annotations,omitempty"`
Platform *platformSpec `json:"platform,omitempty"`
}
type platformSpec struct {
OS string `json:"os"`
Architecture string `json:"architecture"`
Variant string `json:"variant,omitempty"`
}
type layoutIndex struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType,omitempty"`
Manifests []layoutDescriptor `json:"manifests"`
}
// blobPath is where a layout keeps the blob with this digest.
func blobPath(digest string) string {
return "blobs/sha256/" + strings.TrimPrefix(digest, "sha256:")
}
func isManifestType(mt string) bool { return mt == mediaOCIManifest || mt == mediaDockerManifest }
func isIndexType(mt string) bool { return mt == mediaOCIIndex || mt == mediaDockerList }
// readLayoutIndex reads a layout tar's index.json.
func readLayoutIndex(tarPath string) (*layoutIndex, error) {
raw, err := readEntry(tarPath, layoutIndexFile, maxManifestBytes)
if err != nil {
return nil, err
}
var idx layoutIndex
if err := json.Unmarshal(raw, &idx); err != nil {
return nil, fmt.Errorf("imagepush: %s: %s: %w", tarPath, layoutIndexFile, err)
}
return &idx, nil
}
// readLayoutBlob reads a small blob (a manifest, an index, a config) out of a layout
// tar and checks it against its descriptor.
func readLayoutBlob(tarPath string, d layoutDescriptor) ([]byte, error) {
if !sha256DigestRE.MatchString(d.Digest) {
return nil, fmt.Errorf("imagepush: %s: bad digest %q", tarPath, d.Digest)
}
if d.Size < 0 || d.Size > maxManifestBytes {
return nil, fmt.Errorf("imagepush: %s: blob %s has size %d", tarPath, d.Digest, d.Size)
}
body, err := readEntry(tarPath, blobPath(d.Digest), maxManifestBytes)
if err != nil {
return nil, err
}
if got := digestOf(body); got != d.Digest || int64(len(body)) != d.Size {
return nil, fmt.Errorf("imagepush: %s: blob %s (%d bytes) holds %s (%d bytes)", tarPath, d.Digest, d.Size, got, len(body))
}
return body, nil
}
// PushLayout uploads the image the OCI layout tar at tarPath names name (its
// io.containerd.image.name) as ref, and returns the manifest digest the registry
// recorded: the digest of the manifest in the tar, since it is pushed byte for byte.
func (p *Pusher) PushLayout(ctx context.Context, tarPath, name, ref string) (string, error) {
r, err := ParseRef(ref)
if err != nil {
return "", err
}
idx, err := readLayoutIndex(tarPath)
if err != nil {
return "", err
}
var (
desc layoutDescriptor
found int
names []string
)
for _, m := range idx.Manifests {
n := m.Annotations[annotationImageName]
names = append(names, n)
if n == name {
desc = m
found++
}
}
switch {
case found == 0:
sort.Strings(names)
return "", fmt.Errorf("imagepush: %s holds no image named %s (it holds: %s)", tarPath, name, strings.Join(names, ", "))
case found > 1:
return "", fmt.Errorf("imagepush: %s names %d images %s", tarPath, found, name)
}
// A bundle entry points straight at one platform's manifest: that is what makes
// the pushed digest the one containerd imported.
if !isManifestType(desc.MediaType) {
return "", fmt.Errorf("imagepush: %s: %s is a %q, want an image manifest", tarPath, name, desc.MediaType)
}
body, err := readLayoutBlob(tarPath, desc)
if err != nil {
return "", err
}
m, err := parseManifest(body, desc.MediaType)
if err != nil {
return "", fmt.Errorf("imagepush: %s: %s: %w", tarPath, name, err)
}
blobs := append([]descriptor{m.Config}, m.Layers...)
for i, b := range blobs {
err := p.retry(ctx, func() error {
return p.uploadBlob(ctx, r, b.Digest, b.Size, func() (io.ReadCloser, error) {
f, entry, err := openEntry(tarPath, blobPath(b.Digest))
if err != nil {
return nil, err
}
return struct {
io.Reader
io.Closer
}{entry, f}, nil
})
})
if err != nil {
return "", fmt.Errorf("imagepush: %s: blob %d/%d (%s): %w", name, i+1, len(blobs), b.Digest, err)
}
}
var digest string
err = p.retry(ctx, func() error {
d, err := p.putManifest(ctx, r, desc.MediaType, body)
digest = d
return err
})
if err != nil {
return "", fmt.Errorf("imagepush: %s: manifest: %w", r, err)
}
p.logf("pushed %s as %s@%s", name, r, digest)
return digest, nil
}
// parseManifest reads an image manifest's config and layers and checks each is a
// well-formed descriptor. A mediaType field in the body must agree with mt.
func parseManifest(body []byte, mt string) (*manifest, error) {
var m manifest
if err := json.Unmarshal(body, &m); err != nil {
return nil, fmt.Errorf("manifest: %w", err)
}
if m.MediaType != "" && m.MediaType != mt {
return nil, fmt.Errorf("manifest says it is a %q, its descriptor a %q", m.MediaType, mt)
}
if len(m.Layers) == 0 {
return nil, fmt.Errorf("manifest has no layers")
}
for i, b := range append([]descriptor{m.Config}, m.Layers...) {
if !sha256DigestRE.MatchString(b.Digest) || b.Size < 0 {
return nil, fmt.Errorf("blob %d has digest %q size %d", i+1, b.Digest, b.Size)
}
}
return &m, nil
}