Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5a021fe

Browse files
committedJul 31, 2018
Add an oc image extract command that can copy contents out of images to disk
Uses a subset of docker/pkg/archive that is slightly more flexible than the upstream.
1 parent 3f30b36 commit 5a021fe

File tree

14 files changed

+1436
-0
lines changed

14 files changed

+1436
-0
lines changed
 

‎contrib/completions/bash/oc

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

‎contrib/completions/zsh/oc

+56
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/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

+576
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
},

‎test/extended/images/extract.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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=/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=/tmp/test
69+
[ -d /tmp/test/etc ] && [ -d /tmp/test/bin ]
70+
[ -f /tmp/test/bin/ls ] && /tmp/test/bin/ls /tmp/test
71+
`, ns)))
72+
cli.WaitForSuccess(pod.Name, podStartupTimeout)
73+
})
74+
})

0 commit comments

Comments
 (0)
Please sign in to comment.