Skip to content

Commit 5009149

Browse files
authoredAug 14, 2018
Merge pull request #20466 from smarterclayton/extract
Add `oc image extract` to support release tooling
2 parents 14e38d6 + 76fbf4e commit 5009149

File tree

21 files changed

+1837
-673
lines changed

21 files changed

+1837
-673
lines changed
 

‎contrib/completions/bash/oc

+54
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎contrib/completions/zsh/oc

+54
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/man/man1/.files_generated_oc

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/man/man1/oc-image-extract.1

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/oc/cli/image/append/append.go

+15-129
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import (
99
"io/ioutil"
1010
"net/http"
1111
"os"
12-
"regexp"
13-
"runtime"
1412
"strconv"
1513
"time"
1614

@@ -20,8 +18,6 @@ import (
2018

2119
"github.com/docker/distribution"
2220
distributioncontext "github.com/docker/distribution/context"
23-
"github.com/docker/distribution/manifest/manifestlist"
24-
"github.com/docker/distribution/manifest/schema1"
2521
"github.com/docker/distribution/manifest/schema2"
2622
"github.com/docker/distribution/reference"
2723
"github.com/docker/distribution/registry/client"
@@ -38,6 +34,8 @@ import (
3834
"github.com/openshift/origin/pkg/image/dockerlayer/add"
3935
"github.com/openshift/origin/pkg/image/registryclient"
4036
"github.com/openshift/origin/pkg/image/registryclient/dockercredentials"
37+
imagemanifest "github.com/openshift/origin/pkg/oc/cli/image/manifest"
38+
"github.com/openshift/origin/pkg/oc/cli/image/workqueue"
4139
)
4240

4341
var (
@@ -86,10 +84,7 @@ type AppendImageOptions struct {
8684
DropHistory bool
8785
CreatedAt string
8886

89-
OSFilter *regexp.Regexp
90-
DefaultOSFilter bool
91-
92-
FilterByOS string
87+
FilterOptions imagemanifest.FilterOptions
9388

9489
MaxPerRegistry int
9590

@@ -100,12 +95,6 @@ type AppendImageOptions struct {
10095
genericclioptions.IOStreams
10196
}
10297

103-
// schema2ManifestOnly specifically requests a manifest list first
104-
var schema2ManifestOnly = distribution.WithManifestMediaTypes([]string{
105-
manifestlist.MediaTypeManifestList,
106-
schema2.MediaTypeManifest,
107-
})
108-
10998
func NewAppendImageOptions(streams genericclioptions.IOStreams) *AppendImageOptions {
11099
return &AppendImageOptions{
111100
IOStreams: streams,
@@ -129,9 +118,10 @@ func NewCmdAppendImage(name string, streams genericclioptions.IOStreams) *cobra.
129118
}
130119

131120
flag := cmd.Flags()
121+
o.FilterOptions.Bind(flag)
122+
132123
flag.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Print the actions that would be taken and exit without writing to the destination.")
133124
flag.BoolVar(&o.Insecure, "insecure", o.Insecure, "Allow push and pull operations to registries to be made over HTTP")
134-
flag.StringVar(&o.FilterByOS, "filter-by-os", o.FilterByOS, "A regular expression to control which images are mirrored. Images will be passed as '<platform>/<architecture>[/<variant>]'.")
135125

136126
flag.StringVar(&o.From, "from", o.From, "The image to use as a base. If empty, a new scratch image is created.")
137127
flag.StringVar(&o.To, "to", o.To, "The Docker repository tag to upload the appended image to.")
@@ -148,17 +138,8 @@ func NewCmdAppendImage(name string, streams genericclioptions.IOStreams) *cobra.
148138
}
149139

150140
func (o *AppendImageOptions) Complete(cmd *cobra.Command, args []string) error {
151-
pattern := o.FilterByOS
152-
if len(pattern) == 0 && !cmd.Flags().Changed("filter-by-os") {
153-
o.DefaultOSFilter = true
154-
pattern = regexp.QuoteMeta(fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH))
155-
}
156-
if len(pattern) > 0 {
157-
re, err := regexp.Compile(pattern)
158-
if err != nil {
159-
return fmt.Errorf("--filter-by-os was not a valid regular expression: %v", err)
160-
}
161-
o.OSFilter = re
141+
if err := o.FilterOptions.Complete(cmd.Flags()); err != nil {
142+
return err
162143
}
163144

164145
for _, arg := range args {
@@ -175,20 +156,6 @@ func (o *AppendImageOptions) Complete(cmd *cobra.Command, args []string) error {
175156
return nil
176157
}
177158

178-
// includeDescriptor returns true if the provided manifest should be included.
179-
func (o *AppendImageOptions) includeDescriptor(d *manifestlist.ManifestDescriptor, hasMultiple bool) bool {
180-
if o.OSFilter == nil {
181-
return true
182-
}
183-
if o.DefaultOSFilter && !hasMultiple {
184-
return true
185-
}
186-
if len(d.Platform.Variant) > 0 {
187-
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s/%s", d.Platform.OS, d.Platform.Architecture, d.Platform.Variant))
188-
}
189-
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s", d.Platform.OS, d.Platform.Architecture))
190-
}
191-
192159
func (o *AppendImageOptions) Run() error {
193160
var createdAt *time.Time
194161
if len(o.CreatedAt) > 0 {
@@ -256,97 +223,16 @@ func (o *AppendImageOptions) Run() error {
256223
return err
257224
}
258225
fromRepo = repo
259-
var srcDigest digest.Digest
260-
if len(from.Tag) > 0 {
261-
desc, err := repo.Tags(ctx).Get(ctx, from.Tag)
262-
if err != nil {
263-
return err
264-
}
265-
srcDigest = desc.Digest
266-
} else {
267-
srcDigest = digest.Digest(from.ID)
268-
}
269-
manifests, err := repo.Manifests(ctx)
270-
if err != nil {
271-
return err
272-
}
273-
srcManifest, err := manifests.Get(ctx, srcDigest, schema2ManifestOnly)
274-
if err != nil {
275-
return err
276-
}
277226

278-
originalSrcDigest := srcDigest
279-
srcManifests, srcManifest, srcDigest, err := processManifestList(ctx, srcDigest, srcManifest, manifests, *from, o.includeDescriptor)
227+
srcManifest, _, location, err := imagemanifest.FirstManifest(ctx, *from, repo, o.FilterOptions.Include)
280228
if err != nil {
281-
return err
229+
return fmt.Errorf("unable to read image %s: %v", from, err)
282230
}
283-
if len(srcManifests) == 0 {
284-
return fmt.Errorf("filtered all images from %s", from)
285-
}
286-
287-
var location string
288-
if srcDigest == originalSrcDigest {
289-
location = fmt.Sprintf("manifest %s", srcDigest)
290-
} else {
291-
location = fmt.Sprintf("manifest %s in manifest list %s", srcDigest, originalSrcDigest)
231+
base, layers, err = imagemanifest.ManifestToImageConfig(ctx, srcManifest, repo.Blobs(ctx), location)
232+
if err != nil {
233+
return fmt.Errorf("unable to parse image %s: %v", from, err)
292234
}
293235

294-
switch t := srcManifest.(type) {
295-
case *schema2.DeserializedManifest:
296-
if t.Config.MediaType != schema2.MediaTypeImageConfig {
297-
return fmt.Errorf("unable to append layers to images with config %s from %s", t.Config.MediaType, location)
298-
}
299-
configJSON, err := repo.Blobs(ctx).Get(ctx, t.Config.Digest)
300-
if err != nil {
301-
return fmt.Errorf("unable to find manifest for image %s: %v", *from, err)
302-
}
303-
glog.V(4).Infof("Raw image config json:\n%s", string(configJSON))
304-
config := &docker10.DockerImageConfig{}
305-
if err := json.Unmarshal(configJSON, &config); err != nil {
306-
return fmt.Errorf("the source image manifest could not be parsed: %v", err)
307-
}
308-
309-
base = config
310-
layers = t.Layers
311-
base.Size = 0
312-
for _, layer := range t.Layers {
313-
base.Size += layer.Size
314-
}
315-
316-
case *schema1.SignedManifest:
317-
if glog.V(4) {
318-
_, configJSON, _ := srcManifest.Payload()
319-
glog.Infof("Raw image config json:\n%s", string(configJSON))
320-
}
321-
if len(t.History) == 0 {
322-
return fmt.Errorf("input image is in an unknown format: no v1Compatibility history")
323-
}
324-
config := &docker10.DockerV1CompatibilityImage{}
325-
if err := json.Unmarshal([]byte(t.History[0].V1Compatibility), &config); err != nil {
326-
return err
327-
}
328-
329-
base = &docker10.DockerImageConfig{}
330-
if err := docker10.Convert_DockerV1CompatibilityImage_to_DockerImageConfig(config, base); err != nil {
331-
return err
332-
}
333-
334-
// schema1 layers are in reverse order
335-
layers = make([]distribution.Descriptor, 0, len(t.FSLayers))
336-
for i := len(t.FSLayers) - 1; i >= 0; i-- {
337-
layer := distribution.Descriptor{
338-
MediaType: schema2.MediaTypeLayer,
339-
Digest: t.FSLayers[i].BlobSum,
340-
// size must be reconstructed from the blobs
341-
}
342-
// we must reconstruct the tar sum from the blobs
343-
add.AddLayerToConfig(base, layer, "")
344-
layers = append(layers, layer)
345-
}
346-
347-
default:
348-
return fmt.Errorf("unable to append layers to images of type %T from %s", srcManifest, location)
349-
}
350236
} else {
351237
base = add.NewEmptyConfig()
352238
layers = []distribution.Descriptor{add.AddScratchLayerToConfig(base)}
@@ -445,8 +331,8 @@ func (o *AppendImageOptions) Run() error {
445331
// upload base layers in parallel
446332
stopCh := make(chan struct{})
447333
defer close(stopCh)
448-
q := newWorkQueue(o.MaxPerRegistry, stopCh)
449-
err = q.Try(func(w Try) {
334+
q := workqueue.New(o.MaxPerRegistry, stopCh)
335+
err = q.Try(func(w workqueue.Try) {
450336
for i := range layers[:numLayers] {
451337
layer := &layers[i]
452338
index := i
@@ -551,7 +437,7 @@ func (o *AppendImageOptions) Run() error {
551437
if err != nil {
552438
return fmt.Errorf("unable to upload the new image manifest: %v", err)
553439
}
554-
toDigest, err := putManifestInCompatibleSchema(ctx, manifest, to.Tag, toManifests, fromRepo.Blobs(ctx), toRepo.Named())
440+
toDigest, err := imagemanifest.PutManifestInCompatibleSchema(ctx, manifest, to.Tag, toManifests, fromRepo.Blobs(ctx), toRepo.Named())
555441
if err != nil {
556442
return fmt.Errorf("unable to convert the image to a compatible schema version: %v", err)
557443
}

‎pkg/oc/cli/image/append/manifest.go

-178
This file was deleted.

‎pkg/oc/cli/image/archive/archive.go

+438
Large diffs are not rendered by default.
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package archive
2+
3+
import (
4+
"archive/tar"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/docker/docker/pkg/archive"
10+
"github.com/docker/docker/pkg/system"
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
func getWhiteoutConverter(format archive.WhiteoutFormat) tarWhiteoutConverter {
15+
if format == archive.OverlayWhiteoutFormat {
16+
return overlayWhiteoutConverter{}
17+
}
18+
return nil
19+
}
20+
21+
type overlayWhiteoutConverter struct{}
22+
23+
func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, err error) {
24+
// convert whiteouts to AUFS format
25+
if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 {
26+
// we just rename the file and make it normal
27+
dir, filename := filepath.Split(hdr.Name)
28+
hdr.Name = filepath.Join(dir, archive.WhiteoutPrefix+filename)
29+
hdr.Mode = 0600
30+
hdr.Typeflag = tar.TypeReg
31+
hdr.Size = 0
32+
}
33+
34+
if fi.Mode()&os.ModeDir != 0 {
35+
// convert opaque dirs to AUFS format by writing an empty file with the prefix
36+
opaque, err := system.Lgetxattr(path, "trusted.overlay.opaque")
37+
if err != nil {
38+
return nil, err
39+
}
40+
if len(opaque) == 1 && opaque[0] == 'y' {
41+
if hdr.Xattrs != nil {
42+
delete(hdr.Xattrs, "trusted.overlay.opaque")
43+
}
44+
45+
// create a header for the whiteout file
46+
// it should inherit some properties from the parent, but be a regular file
47+
wo = &tar.Header{
48+
Typeflag: tar.TypeReg,
49+
Mode: hdr.Mode & int64(os.ModePerm),
50+
Name: filepath.Join(hdr.Name, archive.WhiteoutOpaqueDir),
51+
Size: 0,
52+
Uid: hdr.Uid,
53+
Uname: hdr.Uname,
54+
Gid: hdr.Gid,
55+
Gname: hdr.Gname,
56+
AccessTime: hdr.AccessTime,
57+
ChangeTime: hdr.ChangeTime,
58+
}
59+
}
60+
}
61+
62+
return
63+
}
64+
65+
func (overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) {
66+
base := filepath.Base(path)
67+
dir := filepath.Dir(path)
68+
69+
// if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay
70+
if base == archive.WhiteoutOpaqueDir {
71+
err := unix.Setxattr(dir, "trusted.overlay.opaque", []byte{'y'}, 0)
72+
// don't write the file itself
73+
return false, err
74+
}
75+
76+
// if a file was deleted and we are using overlay, we need to create a character device
77+
if strings.HasPrefix(base, archive.WhiteoutPrefix) {
78+
originalBase := base[len(archive.WhiteoutPrefix):]
79+
originalPath := filepath.Join(dir, originalBase)
80+
81+
if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil {
82+
return false, err
83+
}
84+
if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil {
85+
return false, err
86+
}
87+
88+
// don't write the file itself
89+
return false, nil
90+
}
91+
92+
return true, nil
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// +build !linux
2+
3+
package archive
4+
5+
import "github.com/docker/docker/pkg/archive"
6+
7+
func getWhiteoutConverter(format archive.WhiteoutFormat) tarWhiteoutConverter {
8+
return nil
9+
}
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// +build !windows
2+
3+
package archive
4+
5+
import (
6+
"archive/tar"
7+
"bufio"
8+
"fmt"
9+
"os"
10+
11+
"github.com/docker/docker/pkg/system"
12+
"golang.org/x/sys/unix"
13+
)
14+
15+
// runningInUserNS detects whether we are currently running in a user namespace.
16+
// Copied from github.com/opencontainers/runc/libcontainer/system
17+
func runningInUserNS() bool {
18+
file, err := os.Open("/proc/self/uid_map")
19+
if err != nil {
20+
// This kernel-provided file only exists if user namespaces are supported
21+
return false
22+
}
23+
defer file.Close()
24+
25+
buf := bufio.NewReader(file)
26+
l, _, err := buf.ReadLine()
27+
if err != nil {
28+
return false
29+
}
30+
31+
line := string(l)
32+
var a, b, c int64
33+
fmt.Sscanf(line, "%d %d %d", &a, &b, &c)
34+
/*
35+
* We assume we are in the initial user namespace if we have a full
36+
* range - 4294967295 uids starting at uid 0.
37+
*/
38+
if a == 0 && b == 0 && c == 4294967295 {
39+
return false
40+
}
41+
return true
42+
}
43+
44+
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
45+
// createTarFile to handle the following types of header: Block; Char; Fifo
46+
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
47+
if runningInUserNS() {
48+
// cannot create a device if running in user namespace
49+
return nil
50+
}
51+
52+
mode := uint32(hdr.Mode & 07777)
53+
switch hdr.Typeflag {
54+
case tar.TypeBlock:
55+
mode |= unix.S_IFBLK
56+
case tar.TypeChar:
57+
mode |= unix.S_IFCHR
58+
case tar.TypeFifo:
59+
mode |= unix.S_IFIFO
60+
}
61+
62+
return system.Mknod(path, mode, int(system.Mkdev(hdr.Devmajor, hdr.Devminor)))
63+
}
64+
65+
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
66+
if hdr.Typeflag == tar.TypeLink {
67+
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
68+
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
69+
return err
70+
}
71+
}
72+
} else if hdr.Typeflag != tar.TypeSymlink {
73+
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
74+
return err
75+
}
76+
}
77+
return nil
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// +build windows
2+
3+
package archive
4+
5+
import (
6+
"archive/tar"
7+
"os"
8+
)
9+
10+
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
11+
// createTarFile to handle the following types of header: Block; Char; Fifo
12+
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
13+
return nil
14+
}
15+
16+
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
17+
return nil
18+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package archive
2+
3+
import (
4+
"syscall"
5+
"time"
6+
)
7+
8+
func timeToTimespec(time time.Time) (ts syscall.Timespec) {
9+
if time.IsZero() {
10+
// Return UTIME_OMIT special value
11+
ts.Sec = 0
12+
ts.Nsec = ((1 << 30) - 2)
13+
return
14+
}
15+
return syscall.NsecToTimespec(time.UnixNano())
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// +build !linux
2+
3+
package archive
4+
5+
import (
6+
"syscall"
7+
"time"
8+
)
9+
10+
func timeToTimespec(time time.Time) (ts syscall.Timespec) {
11+
nsec := int64(0)
12+
if !time.IsZero() {
13+
nsec = time.UnixNano()
14+
}
15+
return syscall.NsecToTimespec(nsec)
16+
}

‎pkg/oc/cli/image/extract/extract.go

+570
Large diffs are not rendered by default.

‎pkg/oc/cli/image/image.go

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/openshift/origin/pkg/cmd/templates"
1313
"github.com/openshift/origin/pkg/oc/cli/image/append"
14+
"github.com/openshift/origin/pkg/oc/cli/image/extract"
1415
"github.com/openshift/origin/pkg/oc/cli/image/mirror"
1516
)
1617

@@ -37,6 +38,7 @@ func NewCmdImage(fullName string, f kcmdutil.Factory, streams genericclioptions.
3738
Message: "Advanced commands:",
3839
Commands: []*cobra.Command{
3940
append.NewCmdAppendImage(name, streams),
41+
extract.New(name, streams),
4042
mirror.NewCmdMirrorImage(name, streams),
4143
},
4244
},

‎pkg/oc/cli/image/manifest/manifest.go

+351
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package manifest
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"regexp"
8+
"runtime"
9+
"sync"
10+
11+
"github.com/spf13/pflag"
12+
13+
"github.com/docker/distribution"
14+
"github.com/docker/distribution/manifest/manifestlist"
15+
"github.com/docker/distribution/manifest/schema1"
16+
"github.com/docker/distribution/manifest/schema2"
17+
"github.com/docker/distribution/reference"
18+
"github.com/docker/distribution/registry/api/errcode"
19+
"github.com/docker/distribution/registry/api/v2"
20+
21+
"github.com/docker/libtrust"
22+
"github.com/golang/glog"
23+
digest "github.com/opencontainers/go-digest"
24+
25+
"github.com/openshift/origin/pkg/image/apis/image/docker10"
26+
imagereference "github.com/openshift/origin/pkg/image/apis/image/reference"
27+
"github.com/openshift/origin/pkg/image/dockerlayer/add"
28+
)
29+
30+
// FilterOptions assist in filtering out unneeded manifests from ManifestList objects.
31+
type FilterOptions struct {
32+
FilterByOS string
33+
DefaultOSFilter bool
34+
OSFilter *regexp.Regexp
35+
}
36+
37+
// Bind adds the options to the flag set.
38+
func (o *FilterOptions) Bind(flags *pflag.FlagSet) {
39+
flags.StringVar(&o.FilterByOS, "filter-by-os", o.FilterByOS, "A regular expression to control which images are mirrored. Images will be passed as '<platform>/<architecture>[/<variant>]'.")
40+
}
41+
42+
// Complete checks whether the flags are ready for use.
43+
func (o *FilterOptions) Complete(flags *pflag.FlagSet) error {
44+
pattern := o.FilterByOS
45+
if len(pattern) == 0 && !flags.Changed("filter-by-os") {
46+
o.DefaultOSFilter = true
47+
pattern = regexp.QuoteMeta(fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH))
48+
}
49+
if len(pattern) > 0 {
50+
re, err := regexp.Compile(pattern)
51+
if err != nil {
52+
return fmt.Errorf("--filter-by-os was not a valid regular expression: %v", err)
53+
}
54+
o.OSFilter = re
55+
}
56+
return nil
57+
}
58+
59+
// Include returns true if the provided manifest should be included, or the first image if the user didn't alter the
60+
// default selection and there is only one image.
61+
func (o *FilterOptions) Include(d *manifestlist.ManifestDescriptor, hasMultiple bool) bool {
62+
if o.OSFilter == nil {
63+
return true
64+
}
65+
if o.DefaultOSFilter && !hasMultiple {
66+
return true
67+
}
68+
if len(d.Platform.Variant) > 0 {
69+
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s/%s", d.Platform.OS, d.Platform.Architecture, d.Platform.Variant))
70+
}
71+
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s", d.Platform.OS, d.Platform.Architecture))
72+
}
73+
74+
// IncludeAll returns true if the provided manifest matches the filter, or all if there was no filter.
75+
func (o *FilterOptions) IncludeAll(d *manifestlist.ManifestDescriptor, hasMultiple bool) bool {
76+
if o.OSFilter == nil {
77+
return true
78+
}
79+
if len(d.Platform.Variant) > 0 {
80+
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s/%s", d.Platform.OS, d.Platform.Architecture, d.Platform.Variant))
81+
}
82+
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s", d.Platform.OS, d.Platform.Architecture))
83+
}
84+
85+
type FilterFunc func(*manifestlist.ManifestDescriptor, bool) bool
86+
87+
// PreferManifestList specifically requests a manifest list first
88+
var PreferManifestList = distribution.WithManifestMediaTypes([]string{
89+
manifestlist.MediaTypeManifestList,
90+
schema2.MediaTypeManifest,
91+
})
92+
93+
// FirstManifest returns the first manifest at the request location that matches the filter function.
94+
func FirstManifest(ctx context.Context, from imagereference.DockerImageReference, repo distribution.Repository, filterFn FilterFunc) (distribution.Manifest, digest.Digest, string, error) {
95+
var srcDigest digest.Digest
96+
if len(from.Tag) > 0 {
97+
desc, err := repo.Tags(ctx).Get(ctx, from.Tag)
98+
if err != nil {
99+
return nil, "", "", err
100+
}
101+
srcDigest = desc.Digest
102+
} else {
103+
srcDigest = digest.Digest(from.ID)
104+
}
105+
manifests, err := repo.Manifests(ctx)
106+
if err != nil {
107+
return nil, "", "", err
108+
}
109+
srcManifest, err := manifests.Get(ctx, srcDigest, PreferManifestList)
110+
if err != nil {
111+
return nil, "", "", err
112+
}
113+
114+
originalSrcDigest := srcDigest
115+
srcManifests, srcManifest, srcDigest, err := ProcessManifestList(ctx, srcDigest, srcManifest, manifests, from, filterFn)
116+
if err != nil {
117+
return nil, "", "", err
118+
}
119+
if len(srcManifests) == 0 {
120+
return nil, "", "", fmt.Errorf("filtered all images from %s", from)
121+
}
122+
123+
var location string
124+
if srcDigest == originalSrcDigest {
125+
location = fmt.Sprintf("manifest %s", srcDigest)
126+
} else {
127+
location = fmt.Sprintf("manifest %s in manifest list %s", srcDigest, originalSrcDigest)
128+
}
129+
return srcManifest, srcDigest, location, nil
130+
}
131+
132+
// ManifestToImageConfig takes an image manifest and converts it into a structured object.
133+
func ManifestToImageConfig(ctx context.Context, srcManifest distribution.Manifest, blobs distribution.BlobService, location string) (*docker10.DockerImageConfig, []distribution.Descriptor, error) {
134+
switch t := srcManifest.(type) {
135+
case *schema2.DeserializedManifest:
136+
if t.Config.MediaType != schema2.MediaTypeImageConfig {
137+
return nil, nil, fmt.Errorf("%s does not have the expected image configuration media type: %s", location, t.Config.MediaType)
138+
}
139+
configJSON, err := blobs.Get(ctx, t.Config.Digest)
140+
if err != nil {
141+
return nil, nil, fmt.Errorf("cannot retrieve image configuration for %s: %v", location, err)
142+
}
143+
glog.V(4).Infof("Raw image config json:\n%s", string(configJSON))
144+
config := &docker10.DockerImageConfig{}
145+
if err := json.Unmarshal(configJSON, &config); err != nil {
146+
return nil, nil, fmt.Errorf("unable to parse image configuration: %v", err)
147+
}
148+
149+
base := config
150+
layers := t.Layers
151+
base.Size = 0
152+
for _, layer := range t.Layers {
153+
base.Size += layer.Size
154+
}
155+
156+
return base, layers, nil
157+
158+
case *schema1.SignedManifest:
159+
if glog.V(4) {
160+
_, configJSON, _ := srcManifest.Payload()
161+
glog.Infof("Raw image config json:\n%s", string(configJSON))
162+
}
163+
if len(t.History) == 0 {
164+
return nil, nil, fmt.Errorf("input image is in an unknown format: no v1Compatibility history")
165+
}
166+
config := &docker10.DockerV1CompatibilityImage{}
167+
if err := json.Unmarshal([]byte(t.History[0].V1Compatibility), &config); err != nil {
168+
return nil, nil, err
169+
}
170+
171+
base := &docker10.DockerImageConfig{}
172+
if err := docker10.Convert_DockerV1CompatibilityImage_to_DockerImageConfig(config, base); err != nil {
173+
return nil, nil, err
174+
}
175+
176+
// schema1 layers are in reverse order
177+
layers := make([]distribution.Descriptor, 0, len(t.FSLayers))
178+
for i := len(t.FSLayers) - 1; i >= 0; i-- {
179+
layer := distribution.Descriptor{
180+
MediaType: schema2.MediaTypeLayer,
181+
Digest: t.FSLayers[i].BlobSum,
182+
// size must be reconstructed from the blobs
183+
}
184+
// we must reconstruct the tar sum from the blobs
185+
add.AddLayerToConfig(base, layer, "")
186+
layers = append(layers, layer)
187+
}
188+
189+
return base, layers, nil
190+
191+
default:
192+
return nil, nil, fmt.Errorf("unknown image manifest of type %T from %s", srcManifest, location)
193+
}
194+
}
195+
196+
func ProcessManifestList(ctx context.Context, srcDigest digest.Digest, srcManifest distribution.Manifest, manifests distribution.ManifestService, ref imagereference.DockerImageReference, filterFn FilterFunc) ([]distribution.Manifest, distribution.Manifest, digest.Digest, error) {
197+
var srcManifests []distribution.Manifest
198+
switch t := srcManifest.(type) {
199+
case *manifestlist.DeserializedManifestList:
200+
manifestDigest := srcDigest
201+
manifestList := t
202+
203+
filtered := make([]manifestlist.ManifestDescriptor, 0, len(t.Manifests))
204+
for _, manifest := range t.Manifests {
205+
if !filterFn(&manifest, len(t.Manifests) > 1) {
206+
glog.V(5).Infof("Skipping image for %#v from %s", manifest.Platform, ref)
207+
continue
208+
}
209+
glog.V(5).Infof("Including image for %#v from %s", manifest.Platform, ref)
210+
filtered = append(filtered, manifest)
211+
}
212+
213+
if len(filtered) == 0 {
214+
return nil, nil, "", nil
215+
}
216+
217+
// if we're filtering the manifest list, update the source manifest and digest
218+
if len(filtered) != len(t.Manifests) {
219+
var err error
220+
t, err = manifestlist.FromDescriptors(filtered)
221+
if err != nil {
222+
return nil, nil, "", fmt.Errorf("unable to filter source image %s manifest list: %v", ref, err)
223+
}
224+
_, body, err := t.Payload()
225+
if err != nil {
226+
return nil, nil, "", fmt.Errorf("unable to filter source image %s manifest list (bad payload): %v", ref, err)
227+
}
228+
manifestList = t
229+
manifestDigest = srcDigest.Algorithm().FromBytes(body)
230+
glog.V(5).Infof("Filtered manifest list to new digest %s:\n%s", manifestDigest, body)
231+
}
232+
233+
for i, manifest := range t.Manifests {
234+
childManifest, err := manifests.Get(ctx, manifest.Digest, distribution.WithManifestMediaTypes([]string{manifestlist.MediaTypeManifestList, schema2.MediaTypeManifest}))
235+
if err != nil {
236+
return nil, nil, "", fmt.Errorf("unable to retrieve source image %s manifest #%d from manifest list: %v", ref, i+1, err)
237+
}
238+
srcManifests = append(srcManifests, childManifest)
239+
}
240+
241+
switch {
242+
case len(srcManifests) == 1:
243+
_, body, err := srcManifests[0].Payload()
244+
if err != nil {
245+
return nil, nil, "", fmt.Errorf("unable to convert source image %s manifest list to single manifest: %v", ref, err)
246+
}
247+
manifestDigest := srcDigest.Algorithm().FromBytes(body)
248+
glog.V(5).Infof("Used only one manifest from the list %s", manifestDigest)
249+
return srcManifests, srcManifests[0], manifestDigest, nil
250+
default:
251+
return append(srcManifests, manifestList), manifestList, manifestDigest, nil
252+
}
253+
254+
default:
255+
return []distribution.Manifest{srcManifest}, srcManifest, srcDigest, nil
256+
}
257+
}
258+
259+
// TDOO: remove when quay.io switches to v2 schema
260+
func PutManifestInCompatibleSchema(
261+
ctx context.Context,
262+
srcManifest distribution.Manifest,
263+
tag string,
264+
toManifests distribution.ManifestService,
265+
// supports schema2 -> schema1 downconversion
266+
blobs distribution.BlobService,
267+
ref reference.Named,
268+
) (digest.Digest, error) {
269+
var options []distribution.ManifestServiceOption
270+
if len(tag) > 0 {
271+
glog.V(5).Infof("Put manifest %s:%s", ref, tag)
272+
options = []distribution.ManifestServiceOption{distribution.WithTag(tag)}
273+
} else {
274+
glog.V(5).Infof("Put manifest %s", ref)
275+
}
276+
toDigest, err := toManifests.Put(ctx, srcManifest, options...)
277+
if err == nil {
278+
return toDigest, nil
279+
}
280+
errs, ok := err.(errcode.Errors)
281+
if !ok || len(errs) == 0 {
282+
return toDigest, err
283+
}
284+
errcode, ok := errs[0].(errcode.Error)
285+
if !ok || errcode.ErrorCode() != v2.ErrorCodeManifestInvalid {
286+
return toDigest, err
287+
}
288+
// try downconverting to v2-schema1
289+
schema2Manifest, ok := srcManifest.(*schema2.DeserializedManifest)
290+
if !ok {
291+
return toDigest, err
292+
}
293+
tagRef, tagErr := reference.WithTag(ref, tag)
294+
if tagErr != nil {
295+
return toDigest, err
296+
}
297+
glog.V(5).Infof("Registry reported invalid manifest error, attempting to convert to v2schema1 as ref %s", tagRef)
298+
schema1Manifest, convertErr := convertToSchema1(ctx, blobs, schema2Manifest, tagRef)
299+
if convertErr != nil {
300+
return toDigest, err
301+
}
302+
if glog.V(6) {
303+
_, data, _ := schema1Manifest.Payload()
304+
glog.Infof("Converted to v2schema1\n%s", string(data))
305+
}
306+
return toManifests.Put(ctx, schema1Manifest, distribution.WithTag(tag))
307+
}
308+
309+
// TDOO: remove when quay.io switches to v2 schema
310+
func convertToSchema1(ctx context.Context, blobs distribution.BlobService, schema2Manifest *schema2.DeserializedManifest, ref reference.Named) (distribution.Manifest, error) {
311+
targetDescriptor := schema2Manifest.Target()
312+
configJSON, err := blobs.Get(ctx, targetDescriptor.Digest)
313+
if err != nil {
314+
return nil, err
315+
}
316+
trustKey, err := loadPrivateKey()
317+
if err != nil {
318+
return nil, err
319+
}
320+
builder := schema1.NewConfigManifestBuilder(blobs, trustKey, ref, configJSON)
321+
for _, d := range schema2Manifest.Layers {
322+
if err := builder.AppendReference(d); err != nil {
323+
return nil, err
324+
}
325+
}
326+
manifest, err := builder.Build(ctx)
327+
if err != nil {
328+
return nil, err
329+
}
330+
return manifest, nil
331+
}
332+
333+
var (
334+
privateKeyLock sync.Mutex
335+
privateKey libtrust.PrivateKey
336+
)
337+
338+
// TDOO: remove when quay.io switches to v2 schema
339+
func loadPrivateKey() (libtrust.PrivateKey, error) {
340+
privateKeyLock.Lock()
341+
defer privateKeyLock.Unlock()
342+
if privateKey != nil {
343+
return privateKey, nil
344+
}
345+
trustKey, err := libtrust.GenerateECP256PrivateKey()
346+
if err != nil {
347+
return nil, err
348+
}
349+
privateKey = trustKey
350+
return privateKey, nil
351+
}

‎pkg/oc/cli/image/mirror/manifest.go

-178
This file was deleted.

‎pkg/oc/cli/image/mirror/mirror.go

+26-47
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"regexp"
87
"time"
98

109
"github.com/docker/distribution"
@@ -27,6 +26,8 @@ import (
2726
imagereference "github.com/openshift/origin/pkg/image/apis/image/reference"
2827
"github.com/openshift/origin/pkg/image/registryclient"
2928
"github.com/openshift/origin/pkg/image/registryclient/dockercredentials"
29+
imagemanifest "github.com/openshift/origin/pkg/oc/cli/image/manifest"
30+
"github.com/openshift/origin/pkg/oc/cli/image/workqueue"
3031
)
3132

3233
var (
@@ -76,9 +77,8 @@ var (
7677

7778
type MirrorImageOptions struct {
7879
Mappings []Mapping
79-
OSFilter *regexp.Regexp
8080

81-
FilterByOS string
81+
FilterOptions imagemanifest.FilterOptions
8282

8383
DryRun bool
8484
Insecure bool
@@ -102,12 +102,6 @@ func NewMirrorImageOptions(streams genericclioptions.IOStreams) *MirrorImageOpti
102102
}
103103
}
104104

105-
// schema2ManifestOnly specifically requests a manifest list first
106-
var schema2ManifestOnly = distribution.WithManifestMediaTypes([]string{
107-
manifestlist.MediaTypeManifestList,
108-
schema2.MediaTypeManifest,
109-
})
110-
111105
// NewCommandMirrorImage copies images from one location to another.
112106
func NewCmdMirrorImage(name string, streams genericclioptions.IOStreams) *cobra.Command {
113107
o := NewMirrorImageOptions(streams)
@@ -118,17 +112,18 @@ func NewCmdMirrorImage(name string, streams genericclioptions.IOStreams) *cobra.
118112
Long: mirrorDesc,
119113
Example: fmt.Sprintf(mirrorExample, name+" mirror"),
120114
Run: func(c *cobra.Command, args []string) {
121-
kcmdutil.CheckErr(o.Complete(args))
115+
kcmdutil.CheckErr(o.Complete(c, args))
122116
kcmdutil.CheckErr(o.Run())
123117
},
124118
}
125119

126120
flag := cmd.Flags()
121+
o.FilterOptions.Bind(flag)
122+
127123
flag.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Print the actions that would be taken and exit without writing to the destinations.")
128124
flag.BoolVar(&o.Insecure, "insecure", o.Insecure, "Allow push and pull operations to registries to be made over HTTP")
129125
flag.BoolVar(&o.SkipMount, "skip-mount", o.SkipMount, "Always push layers instead of cross-mounting them")
130126
flag.BoolVar(&o.SkipMultipleScopes, "skip-multiple-scopes", o.SkipMultipleScopes, "Some registries do not support multiple scopes passed to the registry login.")
131-
flag.StringVar(&o.FilterByOS, "filter-by-os", o.FilterByOS, "A regular expression to control which images are mirrored. Images will be passed as '<platform>/<architecture>[/<variant>]'.")
132127
flag.BoolVar(&o.Force, "force", o.Force, "Attempt to write all layers and manifests even if they exist in the remote repository.")
133128
flag.IntVar(&o.MaxRegistry, "max-registry", 4, "Number of concurrent registries to connect to at any one time.")
134129
flag.IntVar(&o.MaxPerRegistry, "max-per-registry", 6, "Number of concurrent requests allowed per registry.")
@@ -138,7 +133,11 @@ func NewCmdMirrorImage(name string, streams genericclioptions.IOStreams) *cobra.
138133
return cmd
139134
}
140135

141-
func (o *MirrorImageOptions) Complete(args []string) error {
136+
func (o *MirrorImageOptions) Complete(cmd *cobra.Command, args []string) error {
137+
if err := o.FilterOptions.Complete(cmd.Flags()); err != nil {
138+
return err
139+
}
140+
142141
overlap := make(map[string]string)
143142

144143
var err error
@@ -164,15 +163,6 @@ func (o *MirrorImageOptions) Complete(args []string) error {
164163
}
165164
}
166165

167-
pattern := o.FilterByOS
168-
if len(pattern) > 0 {
169-
re, err := regexp.Compile(pattern)
170-
if err != nil {
171-
return fmt.Errorf("--filter-by-os was not a valid regular expression: %v", err)
172-
}
173-
o.OSFilter = re
174-
}
175-
176166
return nil
177167
}
178168

@@ -192,17 +182,6 @@ func (o *MirrorImageOptions) Repository(ctx context.Context, context *registrycl
192182
}
193183
}
194184

195-
// includeDescriptor returns true if the provided manifest should be included.
196-
func (o *MirrorImageOptions) includeDescriptor(d *manifestlist.ManifestDescriptor, hasMultiple bool) bool {
197-
if o.OSFilter == nil {
198-
return true
199-
}
200-
if len(d.Platform.Variant) > 0 {
201-
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s/%s", d.Platform.OS, d.Platform.Architecture, d.Platform.Variant))
202-
}
203-
return o.OSFilter.MatchString(fmt.Sprintf("%s/%s", d.Platform.OS, d.Platform.Architecture))
204-
}
205-
206185
func (o *MirrorImageOptions) Run() error {
207186
start := time.Now()
208187
p, err := o.plan()
@@ -232,10 +211,10 @@ func (o *MirrorImageOptions) Run() error {
232211

233212
stopCh := make(chan struct{})
234213
defer close(stopCh)
235-
q := newWorkQueue(o.MaxRegistry, stopCh)
236-
registryWorkers := make(map[string]*workQueue)
214+
q := workqueue.New(o.MaxRegistry, stopCh)
215+
registryWorkers := make(map[string]workqueue.Interface)
237216
for name := range p.RegistryNames() {
238-
registryWorkers[name] = newWorkQueue(o.MaxPerRegistry, stopCh)
217+
registryWorkers[name] = workqueue.New(o.MaxPerRegistry, stopCh)
239218
}
240219

241220
next := time.Now()
@@ -247,12 +226,12 @@ func (o *MirrorImageOptions) Run() error {
247226
ctx := apirequest.NewContext()
248227
for j := range work.phases {
249228
phase := &work.phases[j]
250-
q.Batch(func(w Work) {
229+
q.Batch(func(w workqueue.Work) {
251230
for i := range phase.independent {
252231
unit := phase.independent[i]
253232
w.Parallel(func() {
254233
// upload blobs
255-
registryWorkers[unit.registry.name].Batch(func(w Work) {
234+
registryWorkers[unit.registry.name].Batch(func(w workqueue.Work) {
256235
for i := range unit.repository.blobs {
257236
op := unit.repository.blobs[i]
258237
for digestString := range op.blobs {
@@ -318,19 +297,19 @@ func (o *MirrorImageOptions) plan() (*plan, error) {
318297

319298
stopCh := make(chan struct{})
320299
defer close(stopCh)
321-
q := newWorkQueue(o.MaxRegistry, stopCh)
322-
registryWorkers := make(map[string]*workQueue)
300+
q := workqueue.New(o.MaxRegistry, stopCh)
301+
registryWorkers := make(map[string]workqueue.Interface)
323302
for name := range tree {
324303
if _, ok := registryWorkers[name.registry]; !ok {
325-
registryWorkers[name.registry] = newWorkQueue(o.MaxPerRegistry, stopCh)
304+
registryWorkers[name.registry] = workqueue.New(o.MaxPerRegistry, stopCh)
326305
}
327306
}
328307

329308
plan := newPlan()
330309

331310
for name := range tree {
332311
src := tree[name]
333-
q.Queue(func(_ Work) {
312+
q.Queue(func(_ workqueue.Work) {
334313
srcRepo, err := fromContext.Repository(ctx, src.ref.DockerClientDefaults().RegistryURL(), src.ref.RepositoryName(), o.Insecure)
335314
if err != nil {
336315
plan.AddError(retrieverError{err: fmt.Errorf("unable to connect to %s: %v", src.ref, err), src: src.ref})
@@ -342,7 +321,7 @@ func (o *MirrorImageOptions) plan() (*plan, error) {
342321
return
343322
}
344323
rq := registryWorkers[name.registry]
345-
rq.Batch(func(w Work) {
324+
rq.Batch(func(w workqueue.Work) {
346325
// convert source tags to digests
347326
for tag := range src.tags {
348327
srcTag, pushTargets := tag, src.tags[tag]
@@ -361,21 +340,21 @@ func (o *MirrorImageOptions) plan() (*plan, error) {
361340

362341
canonicalFrom := srcRepo.Named()
363342

364-
rq.Queue(func(w Work) {
343+
rq.Queue(func(w workqueue.Work) {
365344
for key := range src.digests {
366345
srcDigestString, pushTargets := key, src.digests[key]
367346
w.Parallel(func() {
368347
// load the manifest
369348
srcDigest := godigest.Digest(srcDigestString)
370-
srcManifest, err := manifests.Get(ctx, godigest.Digest(srcDigest), schema2ManifestOnly)
349+
srcManifest, err := manifests.Get(ctx, godigest.Digest(srcDigest), imagemanifest.PreferManifestList)
371350
if err != nil {
372351
plan.AddError(retrieverError{src: src.ref, err: fmt.Errorf("unable to retrieve source image %s manifest %s: %v", src.ref, srcDigest, err)})
373352
return
374353
}
375354

376355
// filter or load manifest list as appropriate
377356
originalSrcDigest := srcDigest
378-
srcManifests, srcManifest, srcDigest, err := processManifestList(ctx, srcDigest, srcManifest, manifests, src.ref, o.includeDescriptor)
357+
srcManifests, srcManifest, srcDigest, err := imagemanifest.ProcessManifestList(ctx, srcDigest, srcManifest, manifests, src.ref, o.FilterOptions.IncludeAll)
379358
if err != nil {
380359
plan.AddError(retrieverError{src: src.ref, err: err})
381360
return
@@ -599,7 +578,7 @@ func copyManifests(
599578
panic(fmt.Sprintf("empty source manifest for %s", srcDigest))
600579
}
601580
for _, tag := range tags.List() {
602-
toDigest, err := putManifestInCompatibleSchema(ctx, srcManifest, tag, plan.to, plan.toBlobs, ref)
581+
toDigest, err := imagemanifest.PutManifestInCompatibleSchema(ctx, srcManifest, tag, plan.to, plan.toBlobs, ref)
603582
if err != nil {
604583
errs = append(errs, fmt.Errorf("unable to push manifest to %s: %v", plan.toRef, err))
605584
continue
@@ -622,7 +601,7 @@ func copyManifests(
622601
if !ok {
623602
panic(fmt.Sprintf("empty source manifest for %s", srcDigest))
624603
}
625-
toDigest, err := putManifestInCompatibleSchema(ctx, srcManifest, "", plan.to, plan.toBlobs, ref)
604+
toDigest, err := imagemanifest.PutManifestInCompatibleSchema(ctx, srcManifest, "", plan.to, plan.toBlobs, ref)
626605
if err != nil {
627606
errs = append(errs, fmt.Errorf("unable to push manifest to %s: %v", plan.toRef, err))
628607
continue

‎pkg/oc/cli/image/mirror/workqueue.go

-131
This file was deleted.

‎pkg/oc/cli/image/append/workqueue.go ‎pkg/oc/cli/image/workqueue/workqueue.go

+17-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
1-
package append
1+
package workqueue
22

33
import (
44
"sync"
55

66
"github.com/golang/glog"
77
)
88

9+
type Work interface {
10+
Parallel(fn func())
11+
}
12+
13+
type Try interface {
14+
Try(fn func() error)
15+
}
16+
17+
type Interface interface {
18+
Batch(func(Work))
19+
Try(func(Try)) error
20+
Queue(func(Work))
21+
Done()
22+
}
23+
924
type workQueue struct {
1025
ch chan workUnit
1126
wg *sync.WaitGroup
1227
}
1328

14-
func newWorkQueue(workers int, stopCh <-chan struct{}) *workQueue {
29+
func New(workers int, stopCh <-chan struct{}) Interface {
1530
q := &workQueue{
1631
ch: make(chan workUnit, 100),
1732
wg: &sync.WaitGroup{},
@@ -77,14 +92,6 @@ type workUnit struct {
7792
wg *sync.WaitGroup
7893
}
7994

80-
type Work interface {
81-
Parallel(fn func())
82-
}
83-
84-
type Try interface {
85-
Try(fn func() error)
86-
}
87-
8895
type worker struct {
8996
wg *sync.WaitGroup
9097
ch chan workUnit

‎test/extended/images/extract.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package images
2+
3+
import (
4+
"github.com/MakeNowJust/heredoc"
5+
g "github.com/onsi/ginkgo"
6+
o "github.com/onsi/gomega"
7+
8+
kapi "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
11+
imageapi "github.com/openshift/api/image/v1"
12+
imageclientset "github.com/openshift/client-go/image/clientset/versioned"
13+
exutil "github.com/openshift/origin/test/extended/util"
14+
)
15+
16+
var _ = g.Describe("[Feature:ImageExtract] Image extract", func() {
17+
defer g.GinkgoRecover()
18+
19+
var oc *exutil.CLI
20+
var ns string
21+
22+
g.AfterEach(func() {
23+
if g.CurrentGinkgoTestDescription().Failed && len(ns) > 0 {
24+
exutil.DumpPodLogsStartingWithInNamespace("", ns, oc)
25+
}
26+
})
27+
28+
oc = exutil.NewCLI("image-extract", exutil.KubeConfigPath())
29+
30+
g.It("should extract content from an image", func() {
31+
ns = oc.Namespace()
32+
cli := oc.KubeFramework().PodClient()
33+
client := imageclientset.NewForConfigOrDie(oc.UserConfig()).Image()
34+
35+
_, err := client.ImageStreamImports(ns).Create(&imageapi.ImageStreamImport{
36+
ObjectMeta: metav1.ObjectMeta{
37+
Name: "1",
38+
},
39+
Spec: imageapi.ImageStreamImportSpec{
40+
Import: true,
41+
Images: []imageapi.ImageImportSpec{
42+
{
43+
From: kapi.ObjectReference{Kind: "DockerImage", Name: "busybox:latest"},
44+
To: &kapi.LocalObjectReference{Name: "busybox"},
45+
},
46+
{
47+
From: kapi.ObjectReference{Kind: "DockerImage", Name: "mysql:latest"},
48+
To: &kapi.LocalObjectReference{Name: "mysql"},
49+
},
50+
},
51+
},
52+
})
53+
o.Expect(err).ToNot(o.HaveOccurred())
54+
55+
// busyboxLayers := isi.Status.Images[0].Image.DockerImageLayers
56+
// busyboxLen := len(busyboxLayers)
57+
// mysqlLayers := isi.Status.Images[1].Image.DockerImageLayers
58+
// mysqlLen := len(mysqlLayers)
59+
60+
pod := cli.Create(cliPodWithPullSecret(oc, heredoc.Docf(`
61+
set -x
62+
63+
# command exits if directory doesn't exist
64+
! oc image extract --insecure docker-registry.default.svc:5000/%[1]s/1:busybox --path=/:/tmp/doesnotexist
65+
66+
# extract busybox to a directory, verify the contents
67+
mkdir -p /tmp/test
68+
oc image extract --insecure docker-registry.default.svc:5000/%[1]s/1:busybox --path=/:/tmp/test
69+
[ -d /tmp/test/etc ] && [ -d /tmp/test/bin ]
70+
[ -f /tmp/test/bin/ls ] && /tmp/test/bin/ls /tmp/test
71+
oc image extract --insecure docker-registry.default.svc:5000/%[1]s/1:busybox --path=/etc/shadow:/tmp --path=/etc/localtime:/tmp
72+
[ -f /tmp/shadow ] && [ -f /tmp/localtime ]
73+
`, ns)))
74+
cli.WaitForSuccess(pod.Name, podStartupTimeout)
75+
})
76+
})

0 commit comments

Comments
 (0)
Please sign in to comment.