feat(imagepush): 写出按架构的发布镜像包并支持按名推送
This commit is contained in:
8 files changed
+1135
-3
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user