Skip to content

Commit 2721b3b

Browse files
Add oc image append which adds layers to a schema1/2 image
This command can take zero or more gzipped layer tars (in Docker layer format) and append them to an existing image or a scratch image and then push the new image to a registry. Layers in the existing image are pushed as well. The caller can mutate the provided config as it goes.
1 parent 1346deb commit 2721b3b

File tree

15 files changed

+1426
-21
lines changed

15 files changed

+1426
-21
lines changed

contrib/completions/bash/oc

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/completions/zsh/oc

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/man/man1/.files_generated_oc

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/man/man1/oc-image-append.1

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package docker10
2+
3+
// Convert_DockerV1CompatibilityImage_to_DockerImageConfig takes a Docker registry digest
4+
// (schema 2.1) and converts it to the external API version of Image.
5+
func Convert_DockerV1CompatibilityImage_to_DockerImageConfig(in *DockerV1CompatibilityImage, out *DockerImageConfig) error {
6+
*out = DockerImageConfig{
7+
ID: in.ID,
8+
Parent: in.Parent,
9+
Comment: in.Comment,
10+
Created: in.Created,
11+
Container: in.Container,
12+
DockerVersion: in.DockerVersion,
13+
Author: in.Author,
14+
Architecture: in.Architecture,
15+
Size: in.Size,
16+
OS: "linux",
17+
ContainerConfig: in.ContainerConfig,
18+
}
19+
if in.Config != nil {
20+
out.Config = &DockerConfig{}
21+
*out.Config = *in.Config
22+
}
23+
return nil
24+
}

pkg/image/dockerlayer/add/add.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package add
2+
3+
import (
4+
"compress/gzip"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"runtime"
10+
"time"
11+
12+
"github.com/docker/distribution"
13+
"github.com/docker/distribution/manifest/schema2"
14+
digest "github.com/opencontainers/go-digest"
15+
16+
"github.com/openshift/origin/pkg/image/apis/image/docker10"
17+
"github.com/openshift/origin/pkg/image/dockerlayer"
18+
)
19+
20+
// get base manifest
21+
// check that I can access base layers
22+
// find the input file (assume I can stream)
23+
// start a streaming upload of the layer to the remote registry, while calculating digests
24+
// get back the final digest
25+
// build the new image manifest and config.json
26+
// upload config.json
27+
// upload the rest of the layers
28+
// tag the image
29+
30+
const (
31+
// dockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers.
32+
dockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
33+
// dockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs.
34+
dockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json"
35+
)
36+
37+
// DigestCopy reads all of src into dst, where src is a gzipped stream. It will return the
38+
// sha256 sum of the underlying content (the layerDigest) and the sha256 sum of the
39+
// tar archive (the blobDigest) or an error. If the gzip layer has a modification time
40+
// it will be returned.
41+
// TODO: use configurable digests
42+
func DigestCopy(dst io.ReaderFrom, src io.Reader) (layerDigest, blobDigest digest.Digest, modTime *time.Time, size int64, err error) {
43+
algo := digest.Canonical
44+
// calculate the blob digest as the sha256 sum of the uploaded contents
45+
blobhash := algo.Hash()
46+
// calculate the diffID as the sha256 sum of the layer contents
47+
pr, pw := io.Pipe()
48+
layerhash := algo.Hash()
49+
ch := make(chan error)
50+
go func() {
51+
defer close(ch)
52+
gr, err := gzip.NewReader(pr)
53+
if err != nil {
54+
ch <- fmt.Errorf("unable to create gzip reader layer upload: %v", err)
55+
return
56+
}
57+
if !gr.Header.ModTime.IsZero() {
58+
modTime = &gr.Header.ModTime
59+
}
60+
_, err = io.Copy(layerhash, gr)
61+
ch <- err
62+
}()
63+
64+
n, err := dst.ReadFrom(io.TeeReader(src, io.MultiWriter(blobhash, pw)))
65+
if err != nil {
66+
return "", "", nil, 0, fmt.Errorf("unable to upload new layer (%d): %v", n, err)
67+
}
68+
if err := pw.Close(); err != nil {
69+
return "", "", nil, 0, fmt.Errorf("unable to complete writing diffID: %v", err)
70+
}
71+
if err := <-ch; err != nil {
72+
return "", "", nil, 0, fmt.Errorf("unable to calculate layer diffID: %v", err)
73+
}
74+
75+
layerDigest = digest.NewDigestFromBytes(algo, layerhash.Sum(make([]byte, 0, layerhash.Size())))
76+
blobDigest = digest.NewDigestFromBytes(algo, blobhash.Sum(make([]byte, 0, blobhash.Size())))
77+
return layerDigest, blobDigest, modTime, n, nil
78+
}
79+
80+
func NewEmptyConfig() *docker10.DockerImageConfig {
81+
config := &docker10.DockerImageConfig{
82+
DockerVersion: "",
83+
// Created must be non-zero
84+
Created: (time.Time{}).Add(1 * time.Second),
85+
OS: runtime.GOOS,
86+
Architecture: runtime.GOARCH,
87+
}
88+
return config
89+
}
90+
91+
func AddScratchLayerToConfig(config *docker10.DockerImageConfig) distribution.Descriptor {
92+
layer := distribution.Descriptor{
93+
MediaType: dockerV2Schema2LayerMediaType,
94+
Digest: digest.Digest(dockerlayer.GzippedEmptyLayerDigest),
95+
Size: int64(len(dockerlayer.GzippedEmptyLayer)),
96+
}
97+
AddLayerToConfig(config, layer, dockerlayer.EmptyLayerDiffID)
98+
return layer
99+
}
100+
101+
func AddLayerToConfig(config *docker10.DockerImageConfig, layer distribution.Descriptor, diffID string) {
102+
if config.RootFS == nil {
103+
config.RootFS = &docker10.DockerConfigRootFS{Type: "layers"}
104+
}
105+
config.RootFS.DiffIDs = append(config.RootFS.DiffIDs, diffID)
106+
config.Size += layer.Size
107+
}
108+
109+
func UploadSchema2Config(ctx context.Context, blobs distribution.BlobService, config *docker10.DockerImageConfig, layers []distribution.Descriptor) (*schema2.DeserializedManifest, error) {
110+
// ensure the image size is correct before persisting
111+
config.Size = 0
112+
for _, layer := range layers {
113+
config.Size += layer.Size
114+
}
115+
configJSON, err := json.Marshal(config)
116+
if err != nil {
117+
return nil, err
118+
}
119+
return putSchema2ImageConfig(ctx, blobs, dockerV2Schema2ConfigMediaType, configJSON, layers)
120+
}
121+
122+
// putSchema2ImageConfig uploads the provided configJSON to the blob store and returns the generated manifest
123+
// for the requested image.
124+
func putSchema2ImageConfig(ctx context.Context, blobs distribution.BlobService, mediaType string, configJSON []byte, layers []distribution.Descriptor) (*schema2.DeserializedManifest, error) {
125+
b := schema2.NewManifestBuilder(blobs, mediaType, configJSON)
126+
for _, layer := range layers {
127+
if err := b.AppendReference(layer); err != nil {
128+
return nil, err
129+
}
130+
}
131+
m, err := b.Build(ctx)
132+
if err != nil {
133+
return nil, err
134+
}
135+
manifest, ok := m.(*schema2.DeserializedManifest)
136+
if !ok {
137+
return nil, fmt.Errorf("unable to turn %T into a DeserializedManifest, unable to store image", m)
138+
}
139+
return manifest, nil
140+
}

0 commit comments

Comments
 (0)