diff --git a/cmd/kpod/history.go b/cmd/kpod/history.go index dd0da38a..9f5c5811 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 00000000..4b41e14a --- /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/kubernetes-incubator/cri-o/libpod" + + "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + importFlags = []cli.Flag{ + cli.StringFlag{ + Name: "change, c", + Usage: "Apply imgspecv1 configurations to the created image", + }, + cli.StringFlag{ + Name: "message, m", + Usage: "Set commit message for image imported", + }, + } + importDescription = "Imports a tarball and saves it as a root filesystem image.\n" + + "The commit message and image config can be modified by the user." + importCommand = cli.Command{ + Name: "import", + Usage: "Import a tarball to create a filesystem image", + Description: importDescription, + Flags: importFlags, + Action: importCmd, + ArgsUsage: "TARBALL", + } +) + +func importCmd(c *cli.Context) error { + if err := validateFlags(c, historyFlags); 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") + case 1: + source = args[0] + case 2: + source = args[0] + opts.Reference = args[1] + default: + return errors.Errorf("too many arguments, need 2 only") + } + + if _, err := url.ParseRequestURI(source); err == nil { + file, err := downloadFromURL(source) + if err != nil { + return nil + } + defer os.Remove(file) + source = file + } + + changes := v1.ImageConfig{} + if c.IsSet("change") { + changes, err = getImageConfig(c.String("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 + + 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(change 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) + + changes := strings.Split(change, " ") + + 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 b8a7b0cb..ab95995f 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/completions/bash/kpod b/completions/bash/kpod index 88edbee6..3325a8a8 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -711,6 +711,28 @@ _kpod_history() { esac } + +_kpod_import() { + local options_with_args=" + --change + -c + --message + -m + " + local boolean_options=" + " + _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 +1424,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 00000000..23855654 --- /dev/null +++ b/docs/kpod-import.1.md @@ -0,0 +1,87 @@ +% 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 and saves it as a filesystem image. +The image configuration can be modified 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 imgspecv1 configurations to the created image +Possible configurations include: +**USER** | **EXPOSE** | **ENV** | **ENTRYPOINT** | **CMD** | **VOLUME** | **WORKDIR** | **LABEL** | **STOPSIGNAL** + +**--message, -m** +Set commit message for image imported + +## EXAMPLES + +``` +# kpod import --change "CMD=/bin/bash ENTRYPOINT=/bin/sh 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://lacrosse.redhat.com/~umohnani/ctr.tar url-image +Downloading from "http://lacrosse.redhat.com/~umohnani/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 diff --git a/libpod/runtime_img.go b/libpod/runtime_img.go index 4e0c02ac..ffc8e591 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 @@ -877,8 +885,70 @@ 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) + annotations[ociv1.AnnotationDescription] = "test image built" + + 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 == "" { + newImg, err := src.NewImage(sc) + 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") + } + reference = "@" + digest.Hex() + } + + policy, err := signature.DefaultPolicy(sc) + if err != nil { + return err + } + + policyContext, err := signature.NewPolicyContext(policy) + 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 diff --git a/test/kpod_import.bats b/test/kpod_import.bats new file mode 100644 index 00000000..24775bdb --- /dev/null +++ b/test/kpod_import.bats @@ -0,0 +1,137 @@ +#!/usr/bin/env bats + +load helpers + +IMAGE="redis:alpine" + +function teardown() { + cleanup_test +} + +@test "kpod import with source and reference" { + 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" { + 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 "" <<< "$images" + echo "$output" + [ "$status" -eq 0 ] + cleanup_ctrs + cleanup_pods + stop_crio + rm -f container.tar +} + +@test "kpod import with message flag" { + 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" { + 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 +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/transports/alltransports/alltransports.go b/vendor/github.com/containers/image/transports/alltransports/alltransports.go index 450e50ab..b4552df6 100644 --- a/vendor/github.com/containers/image/transports/alltransports/alltransports.go +++ b/vendor/github.com/containers/image/transports/alltransports/alltransports.go @@ -13,6 +13,7 @@ import ( _ "github.com/containers/image/oci/archive" _ "github.com/containers/image/oci/layout" _ "github.com/containers/image/openshift" + _ "github.com/containers/image/tarball" // The ostree transport is registered by ostree*.go // The storage transport is registered by storage*.go "github.com/containers/image/transports"