diff options
-rw-r--r-- | cmd/kpod/history.go | 2 | ||||
-rw-r--r-- | cmd/kpod/import.go | 190 | ||||
-rw-r--r-- | cmd/kpod/main.go | 1 | ||||
-rw-r--r-- | cmd/kpod/save.go | 3 | ||||
-rw-r--r-- | completions/bash/kpod | 25 | ||||
-rw-r--r-- | docs/kpod-import.1.md | 88 | ||||
-rw-r--r-- | libpod/runtime_img.go | 125 | ||||
-rw-r--r-- | test/kpod_import.bats | 141 |
8 files changed, 546 insertions, 29 deletions
diff --git a/cmd/kpod/history.go b/cmd/kpod/history.go index ab2115aed..c21c1e338 100644 --- a/cmd/kpod/history.go +++ b/cmd/kpod/history.go @@ -87,7 +87,7 @@ func historyCmd(c *cli.Context) error { runtime, err := getRuntime(c) if err != nil { - return errors.Wrapf(err, "Could not get config") + return errors.Wrapf(err, "could not get runtime") } defer runtime.Shutdown(false) diff --git a/cmd/kpod/import.go b/cmd/kpod/import.go new file mode 100644 index 000000000..2e8702c3d --- /dev/null +++ b/cmd/kpod/import.go @@ -0,0 +1,190 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + + "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/projectatomic/libpod/libpod" + "github.com/urfave/cli" +) + +var ( + importFlags = []cli.Flag{ + cli.StringSliceFlag{ + Name: "change, c", + Usage: "Apply the following possible instructions to the created image (default []): CMD | ENTRYPOINT | ENV | EXPOSE | LABEL | STOPSIGNAL | USER | VOLUME | WORKDIR", + }, + cli.StringFlag{ + Name: "message, m", + Usage: "Set commit message for imported image", + }, + } + importDescription = `Create a container image from the contents of the specified tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz). + Note remote tar balls can be specified, via web address. + Optionally tag the image. You can specify the Dockerfile instructions using the --change option. + ` + importCommand = cli.Command{ + Name: "import", + Usage: "Import a tarball to create a filesystem image", + Description: importDescription, + Flags: importFlags, + Action: importCmd, + ArgsUsage: "TARBALL [REFERENCE]", + } +) + +func importCmd(c *cli.Context) error { + if err := validateFlags(c, importFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + var opts libpod.CopyOptions + var source string + args := c.Args() + switch len(args) { + case 0: + return errors.Errorf("need to give the path to the tarball, or must specify a tarball of '-' for stdin") + case 1: + source = args[0] + case 2: + source = args[0] + opts.Reference = args[1] + default: + return errors.Errorf("too many arguments. Usage TARBALL [REFERENCE]") + } + + changes := v1.ImageConfig{} + if c.IsSet("change") { + changes, err = getImageConfig(c.StringSlice("change")) + if err != nil { + return errors.Wrapf(err, "error adding config changes to image %q", source) + } + } + + history := []v1.History{ + {Comment: c.String("message")}, + } + + config := v1.Image{ + Config: changes, + History: history, + } + + opts.ImageConfig = config + + // if source is a url, download it and save to a temp file + u, err := url.ParseRequestURI(source) + if err == nil && u.Scheme != "" { + file, err := downloadFromURL(source) + if err != nil { + return err + } + defer os.Remove(file) + source = file + } + + return runtime.ImportImage(source, opts) +} + +// donwloadFromURL downloads an image in the format "https:/example.com/myimage.tar" +// and tempoarily saves in it /var/tmp/importxyz, which is deleted after the image is imported +func downloadFromURL(source string) (string, error) { + fmt.Printf("Downloading from %q\n", source) + + outFile, err := ioutil.TempFile("/var/tmp", "import") + if err != nil { + return "", errors.Wrap(err, "error creating file") + } + defer outFile.Close() + + response, err := http.Get(source) + if err != nil { + return "", errors.Wrapf(err, "error downloading %q", source) + } + defer response.Body.Close() + + _, err = io.Copy(outFile, response.Body) + if err != nil { + return "", errors.Wrapf(err, "error saving %q to %q", source, outFile) + } + + return outFile.Name(), nil +} + +// getImageConfig converts the --change flag values in the format "CMD=/bin/bash USER=example" +// to a type v1.ImageConfig +func getImageConfig(changes []string) (v1.ImageConfig, error) { + // USER=value | EXPOSE=value | ENV=value | ENTRYPOINT=value | + // CMD=value | VOLUME=value | WORKDIR=value | LABEL=key=value | STOPSIGNAL=value + + var ( + user string + env []string + entrypoint []string + cmd []string + workingDir string + stopSignal string + ) + + exposedPorts := make(map[string]struct{}) + volumes := make(map[string]struct{}) + labels := make(map[string]string) + + for _, ch := range changes { + pair := strings.Split(ch, "=") + if len(pair) == 1 { + return v1.ImageConfig{}, errors.Errorf("no value given for instruction %q", ch) + } + switch pair[0] { + case "USER": + user = pair[1] + case "EXPOSE": + var st struct{} + exposedPorts[pair[1]] = st + case "ENV": + env = append(env, pair[1]) + case "ENTRYPOINT": + entrypoint = append(entrypoint, pair[1]) + case "CMD": + cmd = append(cmd, pair[1]) + case "VOLUME": + var st struct{} + volumes[pair[1]] = st + case "WORKDIR": + workingDir = pair[1] + case "LABEL": + if len(pair) == 3 { + labels[pair[1]] = pair[2] + } else { + labels[pair[1]] = "" + } + case "STOPSIGNAL": + stopSignal = pair[1] + } + } + + return v1.ImageConfig{ + User: user, + ExposedPorts: exposedPorts, + Env: env, + Entrypoint: entrypoint, + Cmd: cmd, + Volumes: volumes, + WorkingDir: workingDir, + Labels: labels, + StopSignal: stopSignal, + }, nil +} diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index b8a7b0cb5..ab95995fe 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -36,6 +36,7 @@ func main() { exportCommand, historyCommand, imagesCommand, + importCommand, infoCommand, inspectCommand, killCommand, diff --git a/cmd/kpod/save.go b/cmd/kpod/save.go index 287821f0a..0f5fcfa4d 100644 --- a/cmd/kpod/save.go +++ b/cmd/kpod/save.go @@ -91,6 +91,9 @@ func saveCmd(c *cli.Context) error { for _, image := range args { dest := dst + ":" + image if err := runtime.PushImage(image, dest, saveOpts); err != nil { + if err2 := os.Remove(output); err2 != nil { + logrus.Errorf("error deleting %q: %v", output, err) + } return errors.Wrapf(err, "unable to save %q", image) } } diff --git a/completions/bash/kpod b/completions/bash/kpod index 88edbee6f..2387228cb 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -711,6 +711,30 @@ _kpod_history() { esac } + +_kpod_import() { + local options_with_args=" + --change + -c + --message + -m + " + local boolean_options=" + --help + -h + " + _complete_ "$options_with_args" "$boolean_options" + + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __kpod_list_images + ;; + esac +} + _kpod_info() { local boolean_options=" --help @@ -1402,6 +1426,7 @@ _kpod_kpod() { export history images + import info inspect kill diff --git a/docs/kpod-import.1.md b/docs/kpod-import.1.md new file mode 100644 index 000000000..cfcfb6fb2 --- /dev/null +++ b/docs/kpod-import.1.md @@ -0,0 +1,88 @@ +% kpod(1) kpod-import - Simple tool to import a tarball as an image +% Urvashi Mohnani +# kpod-import "1" "November 2017" "kpod" + +## NAME +kpod-import - import a tarball and save it as a filesystem image + +## SYNOPSIS +**kpod import** +**TARBALL** +[**--change**|**-c**] +[**--message**|**-m**] +[**--help**|**-h**] + +## DESCRIPTION +**kpod import** imports a tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) +and saves it as a filesystem image. Remote tarballs can be specified using a URL. +Various image instructions can be configured with the **--change** flag and +a commit message can be set using the **--message** flag. + +**kpod [GLOBAL OPTIONS]** + +**kpod import [GLOBAL OPTIONS]** + +**kpod import [OPTIONS] CONTAINER** + +## OPTIONS + +**--change, -c** +Apply the following possible instructions to the created image: +**CMD** | **ENTRYPOINT** | **ENV** | **EXPOSE** | **LABEL** | **STOPSIGNAL** | **USER** | **VOLUME** | **WORKDIR** +Can be set multiple times + +**--message, -m** +Set commit message for imported image + +## EXAMPLES + +``` +# kpod import --change CMD=/bin/bash --change ENTRYPOINT=/bin/sh --change LABEL=blue=image ctr.tar image-imported +Getting image source signatures +Copying blob sha256:b41deda5a2feb1f03a5c1bb38c598cbc12c9ccd675f438edc6acd815f7585b86 + 25.80 MB / 25.80 MB [======================================================] 0s +Copying config sha256:c16a6d30f3782288ec4e7521c754acc29d37155629cb39149756f486dae2d4cd + 448 B / 448 B [============================================================] 0s +Writing manifest to image destination +Storing signatures +``` + +``` +# cat ctr.tar | kpod import --message "importing the ctr.tar tarball" - image-imported +Getting image source signatures +Copying blob sha256:b41deda5a2feb1f03a5c1bb38c598cbc12c9ccd675f438edc6acd815f7585b86 + 25.80 MB / 25.80 MB [======================================================] 0s +Copying config sha256:af376cdda5c0ac1d9592bf56567253d203f8de6a8edf356c683a645d75221540 + 376 B / 376 B [============================================================] 0s +Writing manifest to image destination +Storing signatures +``` + +``` +# cat ctr.tar | kpod import - +Getting image source signatures +Copying blob sha256:b41deda5a2feb1f03a5c1bb38c598cbc12c9ccd675f438edc6acd815f7585b86 + 25.80 MB / 25.80 MB [======================================================] 0s +Copying config sha256:d61387b4d5edf65edee5353e2340783703074ffeaaac529cde97a8357eea7645 + 378 B / 378 B [============================================================] 0s +Writing manifest to image destination +Storing signatures +``` + +``` +kpod import http://example.com/ctr.tar url-image +Downloading from "http://example.com/ctr.tar" +Getting image source signatures +Copying blob sha256:b41deda5a2feb1f03a5c1bb38c598cbc12c9ccd675f438edc6acd815f7585b86 + 25.80 MB / 25.80 MB [======================================================] 0s +Copying config sha256:5813fe8a3b18696089fd09957a12e88bda43dc1745b5240879ffffe93240d29a + 419 B / 419 B [============================================================] 0s +Writing manifest to image destination +Storing signatures +``` + +## SEE ALSO +kpod(1), kpod-export(1), crio(8), crio.conf(5) + +## HISTORY +November 2017, Originally compiled by Urvashi Mohnani <umohnani@redhat.com> diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index a57aec86c..8a5258e75 100644 --- a/libpod/runtime_img.go +++ b/libpod/runtime_img.go @@ -18,6 +18,7 @@ import ( "github.com/containers/image/pkg/sysregistries" "github.com/containers/image/signature" is "github.com/containers/image/storage" + "github.com/containers/image/tarball" "github.com/containers/image/transports" "github.com/containers/image/transports/alltransports" "github.com/containers/image/types" @@ -49,6 +50,9 @@ var ( DirTransport = "dir" // TransportNames are the supported transports in string form TransportNames = [...]string{DefaultRegistry, DockerArchive, OCIArchive, "ostree:", "dir:"} + // TarballTransport is the transport for importing a tar archive + // and creating a filesystem image + TarballTransport = "tarball" ) // CopyOptions contains the options given when pushing or pulling images @@ -72,6 +76,10 @@ type CopyOptions struct { AuthFile string // Writer is the reportWriter for the output Writer io.Writer + // Reference is the name for the image created when a tar archive is imported + Reference string + // ImageConfig is the Image spec for the image created when a tar archive is imported + ImageConfig ociv1.Image } // Image API @@ -473,23 +481,16 @@ func (r *Runtime) getPullListFromRef(srcRef types.ImageReference, imgName string } // to pull the first image stored in the tar file if len(manifest) == 0 { - // create an image object and use the hex value of the digest as the image ID - // for parsing the store reference - newImg, err := srcRef.NewImage(sc) + // use the hex of the digest if no manifest is found + reference, err := getImageDigest(srcRef, sc) if err != nil { return nil, err } - defer newImg.Close() - digest := newImg.ConfigInfo().Digest - if err := digest.Validate(); err == nil { - pullInfo, err := r.getPullStruct(srcRef, "@"+digest.Hex()) - if err != nil { - return nil, err - } - pullStructs = append(pullStructs, pullInfo) - } else { - return nil, errors.Wrapf(err, "error getting config info") + pullInfo, err := r.getPullStruct(srcRef, reference) + if err != nil { + return nil, err } + pullStructs = append(pullStructs, pullInfo) } else { pullInfo, err := r.getPullStruct(srcRef, manifest[0].RepoTags[0]) if err != nil { @@ -497,7 +498,6 @@ func (r *Runtime) getPullListFromRef(srcRef types.ImageReference, imgName string } pullStructs = append(pullStructs, pullInfo) } - } else if srcRef.Transport().Name() == OCIArchive { // retrieve the manifest from index.json to access the image name manifest, err := ociarchive.LoadManifestDescriptor(srcRef) @@ -572,12 +572,7 @@ func (r *Runtime) PullImage(imgName string, options CopyOptions) error { } } - policy, err := signature.DefaultPolicy(sc) - if err != nil { - return err - } - - policyContext, err := signature.NewPolicyContext(policy) + policyContext, err := getPolicyContext(sc) if err != nil { return err } @@ -628,12 +623,7 @@ func (r *Runtime) PushImage(source string, destination string, options CopyOptio sc := common.GetSystemContext(signaturePolicyPath, options.AuthFile) - policy, err := signature.DefaultPolicy(sc) - if err != nil { - return err - } - - policyContext, err := signature.NewPolicyContext(policy) + policyContext, err := getPolicyContext(sc) if err != nil { return err } @@ -880,8 +870,57 @@ func (r *Runtime) GetHistory(image string) ([]ociv1.History, []types.BlobInfo, s } // ImportImage imports an OCI format image archive into storage as an image -func (r *Runtime) ImportImage(path string) (*storage.Image, error) { - return nil, ErrNotImplemented +func (r *Runtime) ImportImage(path string, options CopyOptions) error { + r.lock.RLock() + defer r.lock.RUnlock() + + if !r.valid { + return ErrRuntimeStopped + } + + file := TarballTransport + ":" + path + src, err := alltransports.ParseImageName(file) + if err != nil { + return errors.Wrapf(err, "error parsing image name %q", path) + } + + updater, ok := src.(tarball.ConfigUpdater) + if !ok { + return errors.Wrapf(err, "unexpected type, a tarball reference should implement tarball.ConfigUpdater") + } + + annotations := make(map[string]string) + + err = updater.ConfigUpdate(options.ImageConfig, annotations) + if err != nil { + return errors.Wrapf(err, "error updating image config") + } + + var reference = options.Reference + sc := common.GetSystemContext("", "") + + // if reference not given, get the image digest + if reference == "" { + reference, err = getImageDigest(src, sc) + if err != nil { + return err + } + } + + policyContext, err := getPolicyContext(sc) + if err != nil { + return err + } + defer policyContext.Destroy() + + copyOptions := common.GetCopyOptions(os.Stdout, "", nil, nil, common.SigningOptions{}, "") + + dest, err := is.Transport.ParseStoreReference(r.store, reference) + if err != nil { + errors.Wrapf(err, "error getting image reference for %q", options.Reference) + } + + return cp.Image(policyContext, dest, src, copyOptions) } // GetImageInspectInfo returns the inspect information of an image @@ -1083,3 +1122,33 @@ func findImageInSlice(images []storage.Image, ref string) (storage.Image, error) } return storage.Image{}, errors.New("could not find image") } + +// getImageDigest creates an image object and uses the hex value of the digest as the image ID +// for parsing the store reference +func getImageDigest(src types.ImageReference, ctx *types.SystemContext) (string, error) { + newImg, err := src.NewImage(ctx) + if err != nil { + return "", err + } + defer newImg.Close() + + digest := newImg.ConfigInfo().Digest + if err = digest.Validate(); err != nil { + return "", errors.Wrapf(err, "error getting config info") + } + return "@" + digest.Hex(), nil +} + +// getPolicyContext sets up, intializes and returns a new context for the specified policy +func getPolicyContext(ctx *types.SystemContext) (*signature.PolicyContext, error) { + policy, err := signature.DefaultPolicy(ctx) + if err != nil { + return nil, err + } + + policyContext, err := signature.NewPolicyContext(policy) + if err != nil { + return nil, err + } + return policyContext, nil +} diff --git a/test/kpod_import.bats b/test/kpod_import.bats new file mode 100644 index 000000000..03a89f2e8 --- /dev/null +++ b/test/kpod_import.bats @@ -0,0 +1,141 @@ +#!/usr/bin/env bats + +load helpers + +IMAGE="redis:alpine" + +function teardown() { + cleanup_test +} + +@test "kpod import with source and reference" { + skip "Test needs to be converted to kpod run" + start_crio + run crioctl pod run --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run crioctl image pull "$IMAGE" + echo "$output" + [ "$status" -eq 0 ] + run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ${KPOD_BINARY} ${KPOD_OPTIONS} export -o container.tar "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} import container.tar imported-image + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} images + echo "$output" + [ "$status" -eq 0 ] + images="$output" + run grep "imported-image" <<< "$images" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_crio + rm -f container.tar +} + +@test "kpod import without reference" { + skip "Test needs to be converted to kpod run" + start_crio + run crioctl pod run --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run crioctl image pull "$IMAGE" + echo "$output" + [ "$status" -eq 0 ] + run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ${KPOD_BINARY} ${KPOD_OPTIONS} export -o container.tar "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} import container.tar + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} images + echo "$output" + [ "$status" -eq 0 ] + images="$output" + run grep "<none>" <<< "$images" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_crio + rm -f container.tar +} + +@test "kpod import with message flag" { + skip "Test needs to be converted to kpod run" + start_crio + run crioctl pod run --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run crioctl image pull "$IMAGE" + echo "$output" + [ "$status" -eq 0 ] + run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ${KPOD_BINARY} ${KPOD_OPTIONS} export -o container.tar "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} import --message "importing container test message" container.tar imported-image + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} history imported-image + echo "$output" + [ "$status" -eq 0 ] + history="$output" + run grep "importing container test message" <<< "$history" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_crio + rm -f container.tar +} + +@test "kpod import with change flag" { + skip "Test needs to be converted to kpod run" + start_crio + run crioctl pod run --config "$TESTDATA"/sandbox_config.json + echo "$output" + [ "$status" -eq 0 ] + pod_id="$output" + run crioctl image pull "$IMAGE" + echo "$output" + [ "$status" -eq 0 ] + run crioctl ctr create --config "$TESTDATA"/container_config.json --pod "$pod_id" + echo "$output" + [ "$status" -eq 0 ] + ctr_id="$output" + run ${KPOD_BINARY} ${KPOD_OPTIONS} export -o container.tar "$ctr_id" + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} import --change "CMD=/bin/bash" container.tar imported-image + echo "$output" + [ "$status" -eq 0 ] + run ${KPOD_BINARY} ${KPOD_OPTIONS} inspect imported-image + echo "$output" + [ "$status" -eq 0 ] + inspect="$output" + run grep "/bin/bash" <<< "$inspect" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_crio + rm -f container.tar +} |