Skip to content

Commit 91ef8b3

Browse files
Add --file to image extraction to simplify getting single files
As a user, if i want a single file out of an image it's annoying. Make extraction explicit (does not require confirm) if you want to extract a single file into the current directory. oc image extract IMAGE --file /usr/bin/ls copies ls into the current directory.
1 parent c4b9808 commit 91ef8b3

File tree

4 files changed

+57
-20
lines changed

4 files changed

+57
-20
lines changed

contrib/completions/bash/oc

Lines changed: 2 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: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ type TarEntryFunc func(*tar.Header, LayerInfo, io.Reader) (cont bool, err error)
9696
type Options struct {
9797
Mappings []Mapping
9898

99+
Files []string
99100
Paths []string
100101

101102
OnlyFiles bool
@@ -125,7 +126,7 @@ type Options struct {
125126

126127
func NewOptions(streams genericclioptions.IOStreams) *Options {
127128
return &Options{
128-
Paths: []string{"/:."},
129+
Paths: []string{},
129130

130131
IOStreams: streams,
131132
MaxPerRegistry: 1,
@@ -155,6 +156,7 @@ func New(name string, streams genericclioptions.IOStreams) *cobra.Command {
155156
flag.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Print the actions that would be taken and exit without writing any contents.")
156157
flag.BoolVar(&o.Insecure, "insecure", o.Insecure, "Allow pull operations to registries to be made over HTTP")
157158

159+
flag.StringSliceVar(&o.Files, "file", o.Files, "Extract the specified files to the current directory.")
158160
flag.StringSliceVar(&o.Paths, "path", o.Paths, "Extract only part of an image. Must be SRC:DST where SRC is the path within the image and DST a local directory. If not specified the default is to extract everything to the current directory.")
159161
flag.BoolVarP(&o.PreservePermissions, "preserve-ownership", "p", o.PreservePermissions, "Preserve the permissions of extracted files.")
160162
flag.BoolVar(&o.OnlyFiles, "only-files", o.OnlyFiles, "Only extract regular files and directories from the image.")
@@ -184,11 +186,24 @@ type Mapping struct {
184186
ConditionFn func(m *Mapping, dgst digest.Digest, imageConfig *docker10.DockerImageConfig) (bool, error)
185187
}
186188

187-
func parseMappings(images, paths []string, requireEmpty bool) ([]Mapping, error) {
189+
func parseMappings(images, paths, files []string, requireEmpty bool) ([]Mapping, error) {
188190
layerFilter := regexp.MustCompile(`^(.*)\[([^\]]*)\](.*)$`)
189191

190192
var mappings []Mapping
193+
194+
// convert paths and files to mappings for each image
191195
for _, image := range images {
196+
for _, arg := range files {
197+
if strings.HasSuffix(arg, "/") {
198+
return nil, fmt.Errorf("invalid file: %s must not end with a slash", arg)
199+
}
200+
mappings = append(mappings, Mapping{
201+
Image: image,
202+
From: strings.TrimPrefix(arg, "/"),
203+
To: ".",
204+
})
205+
}
206+
192207
for _, arg := range paths {
193208
parts := strings.SplitN(arg, ":", 2)
194209
var mapping Mapping
@@ -198,17 +213,6 @@ func parseMappings(images, paths []string, requireEmpty bool) ([]Mapping, error)
198213
default:
199214
return nil, fmt.Errorf("--paths must be of the form SRC:DST")
200215
}
201-
if matches := layerFilter.FindStringSubmatch(mapping.Image); len(matches) > 0 {
202-
if len(matches[1]) == 0 || len(matches[2]) == 0 || len(matches[3]) != 0 {
203-
return nil, fmt.Errorf("layer selectors must be of the form IMAGE[\\d:\\d]")
204-
}
205-
mapping.Image = matches[1]
206-
var err error
207-
mapping.LayerFilter, err = parseLayerFilter(matches[2])
208-
if err != nil {
209-
return nil, err
210-
}
211-
}
212216
if len(mapping.From) > 0 {
213217
mapping.From = strings.TrimPrefix(mapping.From, "/")
214218
}
@@ -238,17 +242,35 @@ func parseMappings(images, paths []string, requireEmpty bool) ([]Mapping, error)
238242
}
239243
}
240244
}
241-
src, err := imagereference.Parse(mapping.Image)
245+
}
246+
}
247+
248+
// extract layer filter and set the ref
249+
for i := range mappings {
250+
mapping := &mappings[i]
251+
252+
if matches := layerFilter.FindStringSubmatch(mapping.Image); len(matches) > 0 {
253+
if len(matches[1]) == 0 || len(matches[2]) == 0 || len(matches[3]) != 0 {
254+
return nil, fmt.Errorf("layer selectors must be of the form IMAGE[\\d:\\d]")
255+
}
256+
mapping.Image = matches[1]
257+
var err error
258+
mapping.LayerFilter, err = parseLayerFilter(matches[2])
242259
if err != nil {
243260
return nil, err
244261
}
245-
if len(src.Tag) == 0 && len(src.ID) == 0 {
246-
return nil, fmt.Errorf("source image must point to an image ID or image tag")
247-
}
248-
mapping.ImageRef = src
249-
mappings = append(mappings, mapping)
250262
}
263+
264+
src, err := imagereference.Parse(mapping.Image)
265+
if err != nil {
266+
return nil, err
267+
}
268+
if len(src.Tag) == 0 && len(src.ID) == 0 {
269+
return nil, fmt.Errorf("source image must point to an image ID or image tag")
270+
}
271+
mapping.ImageRef = src
251272
}
273+
252274
return mappings, nil
253275
}
254276

@@ -261,8 +283,12 @@ func (o *Options) Complete(cmd *cobra.Command, args []string) error {
261283
return fmt.Errorf("you must specify at least one image to extract as an argument")
262284
}
263285

286+
if len(o.Paths) == 0 && len(o.Files) == 0 {
287+
o.Paths = append(o.Paths, "/:.")
288+
}
289+
264290
var err error
265-
o.Mappings, err = parseMappings(args, o.Paths, !o.Confirm && !o.DryRun)
291+
o.Mappings, err = parseMappings(args, o.Paths, o.Files, !o.Confirm && !o.DryRun)
266292
if err != nil {
267293
return err
268294
}

test/extended/images/extract.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ var _ = g.Describe("[Feature:ImageExtract] Image extract", func() {
7878
[ -d /tmp/test/etc ] && [ -d /tmp/test/bin ]
7979
[ -f /tmp/test/bin/ls ] && /tmp/test/bin/ls /tmp/test
8080
81+
# extract multiple individual files
8182
mkdir -p /tmp/test2
8283
oc image extract --insecure %[2]s/%[1]s/1:busybox --path=/etc/shadow:/tmp/test2 --path=/etc/localtime:/tmp/test2
8384
[ -f /tmp/test2/shadow ] && [ -f /tmp/test2/localtime ]
85+
86+
# extract a single file to the current directory
87+
mkdir -p /tmp/test3
88+
cd /tmp/test3
89+
oc image extract --insecure %[2]s/%[1]s/1:busybox --file=/etc/shadow
90+
[ -f /tmp/test3/shadow ]
8491
`, ns, registry)))
8592
cli.WaitForSuccess(pod.Name, podStartupTimeout)
8693
})

0 commit comments

Comments
 (0)