From fc7b7d8d3a339b62674cc10ed8ec7701ebe971a3 Mon Sep 17 00:00:00 2001 From: Vito Caputo Date: Thu, 26 Mar 2015 11:58:08 -0700 Subject: [PATCH 1/2] rkt: implement positional --mount --mount flags preceding any apps apply globally to all apps, --mount flags succeeding any apps apply only to the nearest preceding app. also moves the volume list into rktApps. --- common/apps/apps.go | 12 ++++--- rkt/cli_apps.go | 78 +++++++++++++++++++++++++++++++++++++++- rkt/prepare.go | 4 +-- rkt/run.go | 35 +++++------------- stage0/run.go | 12 +++++-- stage1/init/pod.go | 18 ++++++---- stage1/init/pod_test.go | 10 +++++- tests/rkt_volume_test.go | 10 +++--- 8 files changed, 130 insertions(+), 49 deletions(-) diff --git a/common/apps/apps.go b/common/apps/apps.go index 7b08384303..9e68c30b54 100644 --- a/common/apps/apps.go +++ b/common/apps/apps.go @@ -17,20 +17,24 @@ package apps import ( + "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema" "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) type App struct { - Image string // the image reference as supplied by the user on the cli - Args []string // any arguments the user supplied for this app - Asc string // signature file override for image verification (if fetching occurs) + Image string // the image reference as supplied by the user on the cli + Args []string // any arguments the user supplied for this app + Asc string // signature file override for image verification (if fetching occurs) + Mounts []schema.Mount // mounts for this app (superceding any mounts in rktApps.mounts of same MountPoint) // TODO(jonboulle): These images are partially-populated hashes, this should be clarified. ImageID types.Hash // resolved image identifier } type Apps struct { - apps []App + apps []App + Mounts []schema.Mount // global mounts applied to all apps + Volumes []types.Volume // volumes available to all apps } // Reset creates a new slice for al.apps, needed by tests diff --git a/rkt/cli_apps.go b/rkt/cli_apps.go index 8a9b7a38c8..4c5d2ed0d0 100644 --- a/rkt/cli_apps.go +++ b/rkt/cli_apps.go @@ -19,7 +19,11 @@ package main import ( "flag" "fmt" + "net/url" + "strings" + "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema" + "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types" "github.com/coreos/rkt/common/apps" ) @@ -126,4 +130,76 @@ func (al *appAsc) String() string { return app.Asc } -// TODO(vc): --mount, --set-env, etc. +// appMount is for --mount flags in the form of: --mount volume=VOLNAME,target=MNTNAME +type appMount apps.Apps + +func (al *appMount) Set(s string) error { + mount := schema.Mount{} + + // this is intentionally made similar to types.VolumeFromString() + m, err := url.ParseQuery(strings.Replace(s, ",", "&", -1)) + if err != nil { + return err + } + + for key, val := range m { + if len(val) > 1 { + return fmt.Errorf("label %s with multiple values %q", key, val) + } + switch key { + // FIXME(vc): ACName seems a bit restrictive for naming volumes and mountpoints... + case "volume": + mv, err := types.NewACName(val[0]) + if err != nil { + return err + } + mount.Volume = *mv + case "target": + mp, err := types.NewACName(val[0]) + if err != nil { + return err + } + mount.MountPoint = *mp + default: + return fmt.Errorf("unknown mount parameter %q", key) + } + } + + if (*apps.Apps)(al).Count() == 0 { + (*apps.Apps)(al).Mounts = append((*apps.Apps)(al).Mounts, mount) + } else { + app := (*apps.Apps)(al).Last() + app.Mounts = append(app.Mounts, mount) + } + + return nil +} + +func (al *appMount) String() string { + var ms []string + for _, m := range ((*apps.Apps)(al)).Mounts { + ms = append(ms, m.Volume.String(), ":", m.MountPoint.String()) + } + return strings.Join(ms, " ") +} + +// appsVolume is for --volume flags in the form name,kind=host,source=/tmp,readOnly=true (defined by appc) +type appsVolume apps.Apps + +func (al *appsVolume) Set(s string) error { + vol, err := types.VolumeFromString(s) + if err != nil { + return err + } + + (*apps.Apps)(al).Volumes = append((*apps.Apps)(al).Volumes, *vol) + return nil +} + +func (al *appsVolume) String() string { + var vs []string + for _, v := range (*apps.Apps)(al).Volumes { + vs = append(vs, v.String()) + } + return strings.Join(vs, " ") +} diff --git a/rkt/prepare.go b/rkt/prepare.go index a51c77bad9..bfc910d1f7 100644 --- a/rkt/prepare.go +++ b/rkt/prepare.go @@ -51,13 +51,14 @@ End the image arguments with a lone "---" to resume argument parsing.`, func init() { commands = append(commands, cmdPrepare) prepareFlags.StringVar(&flagStage1Image, "stage1-image", defaultStage1Image, `image to use as stage1. Local paths and http/https URLs are supported. If empty, rkt will look for a file called "stage1.aci" in the same directory as rkt itself`) - prepareFlags.Var(&flagVolumes, "volume", "volumes to mount into the pod") prepareFlags.Var(&flagPorts, "port", "ports to expose on the host (requires --private-net)") prepareFlags.BoolVar(&flagQuiet, "quiet", false, "suppress superfluous output on stdout, print only the UUID on success") prepareFlags.BoolVar(&flagInheritEnv, "inherit-env", false, "inherit all environment variables not set by apps") prepareFlags.BoolVar(&flagNoOverlay, "no-overlay", false, "disable overlay filesystem") prepareFlags.Var(&flagExplicitEnv, "set-env", "an environment variable to set for apps in the form name=value") prepareFlags.BoolVar(&flagLocal, "local", false, "use only local images (do not discover or download from remote URLs)") + prepareFlags.Var((*appsVolume)(&rktApps), "volume", "volumes to make available in the pod") + prepareFlags.Var((*appMount)(&rktApps), "mount", "mount point binding a volume to a path within an app") } func runPrepare(args []string) (exit int) { @@ -133,7 +134,6 @@ func runPrepare(args []string) (exit int) { Stage1Image: *s1img, UUID: p.uuid, }, - Volumes: []types.Volume(flagVolumes), Ports: []types.ExposedPort(flagPorts), InheritEnv: flagInheritEnv, ExplicitEnv: flagExplicitEnv.Strings(), diff --git a/rkt/run.go b/rkt/run.go index dd38c031cd..2a92744b23 100644 --- a/rkt/run.go +++ b/rkt/run.go @@ -39,10 +39,15 @@ var ( cmdRun = &Command{ Name: "run", Summary: "Run image(s) in a pod in rkt", - Usage: "[--volume name,kind=host,...] IMAGE [-- image-args...[---]]...", + Usage: "[--volume VOL,kind=host,...] [--mount volume=VOL,target=PATH] IMAGE [-- image-args...[---]]...", Description: `IMAGE should be a string referencing an image; either a hash, local file on disk, or URL. They will be checked in that order and the first match will be used. +Volumes are made available to the container via --volume. +Mounts bind volumes into each image's root within the container via --mount. +--mount is position-sensitive; occuring before any images applies to all images, +occuring after any images applies only to the nearest preceding image. + An "--" may be used to inhibit rkt run's parsing of subsequent arguments, which will instead be appended to the preceding image app's exec arguments. End the image arguments with a lone "---" to resume argument parsing.`, @@ -52,7 +57,6 @@ End the image arguments with a lone "---" to resume argument parsing.`, } runFlags flag.FlagSet flagStage1Image string - flagVolumes volumeList flagPorts portList flagPrivateNet bool flagInheritEnv bool @@ -74,7 +78,6 @@ func init() { } runFlags.StringVar(&flagStage1Image, "stage1-image", defaultStage1Image, `image to use as stage1. Local paths and http/https URLs are supported. If empty, rkt will look for a file called "stage1.aci" in the same directory as rkt itself`) - runFlags.Var(&flagVolumes, "volume", "volumes to mount into the pod") runFlags.Var(&flagPorts, "port", "ports to expose on the host (requires --private-net)") runFlags.BoolVar(&flagPrivateNet, "private-net", false, "give pod a private network") runFlags.BoolVar(&flagInheritEnv, "inherit-env", false, "inherit all environment variables not set by apps") @@ -83,7 +86,8 @@ func init() { runFlags.BoolVar(&flagInteractive, "interactive", false, "run pod interactively") runFlags.Var((*appAsc)(&rktApps), "signature", "local signature file to use in validating the preceding image") runFlags.BoolVar(&flagLocal, "local", false, "use only local images (do not discover or download from remote URLs)") - flagVolumes = volumeList{} + runFlags.Var((*appsVolume)(&rktApps), "volume", "volumes to make available in the pod") + runFlags.Var((*appMount)(&rktApps), "mount", "mount point binding a volume to a path within an app") flagPorts = portList{} } @@ -167,7 +171,6 @@ func runRun(args []string) (exit int) { pcfg := stage0.PrepareConfig{ CommonConfig: cfg, Apps: &rktApps, - Volumes: []types.Volume(flagVolumes), Ports: []types.ExposedPort(flagPorts), InheritEnv: flagInheritEnv, ExplicitEnv: flagExplicitEnv.Strings(), @@ -204,28 +207,6 @@ func runRun(args []string) (exit int) { return 1 } -// volumeList implements the flag.Value interface to contain a set of mappings -// from mount label --> mount path -type volumeList []types.Volume - -func (vl *volumeList) Set(s string) error { - vol, err := types.VolumeFromString(s) - if err != nil { - return err - } - - *vl = append(*vl, *vol) - return nil -} - -func (vl *volumeList) String() string { - var vs []string - for _, v := range []types.Volume(*vl) { - vs = append(vs, v.String()) - } - return strings.Join(vs, " ") -} - // portList implements the flag.Value interface to contain a set of mappings // from port name --> host port type portList []types.ExposedPort diff --git a/stage0/run.go b/stage0/run.go index 9d7521d50a..3c881dc8a8 100644 --- a/stage0/run.go +++ b/stage0/run.go @@ -52,7 +52,6 @@ type PrepareConfig struct { Apps *apps.Apps // apps to prepare InheritEnv bool // inherit parent environment into apps ExplicitEnv []string // always set these environment variables for all the apps - Volumes []types.Volume // list of volumes that rkt can provide to applications Ports []types.ExposedPort // list of ports that rkt will expose on the host UseOverlay bool // prepare pod with overlay fs } @@ -100,6 +99,14 @@ func MergeEnvs(appEnv *types.Environment, inheritEnv bool, setEnv []string) { } } +// MergeMounts combines the global and per-app mount slices +func MergeMounts(mounts []schema.Mount, appMounts []schema.Mount) []schema.Mount { + ml := mounts + ml = append(ml, appMounts...) + // TODO(vc): deduplicate mountpoint collisions? (prioritize appMounts?) + return ml +} + // Prepare sets up a pod based on the given config. func Prepare(cfg PrepareConfig, dir string, uuid *types.UUID) error { log.Printf("Preparing stage1") @@ -138,6 +145,7 @@ func Prepare(cfg PrepareConfig, dir string, uuid *types.UUID) error { ID: img, }, Annotations: am.Annotations, + Mounts: MergeMounts(cfg.Apps.Mounts, app.Mounts), } if execAppends := app.Args; execAppends != nil { @@ -159,7 +167,7 @@ func Prepare(cfg PrepareConfig, dir string, uuid *types.UUID) error { // TODO(jonboulle): check that app mountpoint expectations are // satisfied here, rather than waiting for stage1 - cm.Volumes = cfg.Volumes + cm.Volumes = cfg.Apps.Volumes cm.Ports = cfg.Ports cdoc, err := json.Marshal(cm) diff --git a/stage1/init/pod.go b/stage1/init/pod.go index f8aa930179..39d04ed2ee 100644 --- a/stage1/init/pod.go +++ b/stage1/init/pod.go @@ -292,20 +292,24 @@ func (p *Pod) appToNspawnArgs(ra *schema.RuntimeApp, am *schema.ImageManifest) ( vols := make(map[types.ACName]types.Volume) - // TODO(philips): this is implicitly creating a mapping from MountPoint - // to volumes. This is a nice convenience for users but we will need to - // introduce a --mount flag so they can control which mountPoint maps to - // which volume. - + // Here we bind the volumes to the mountpoints via runtime mounts (--mount) for _, v := range p.Manifest.Volumes { vols[v.Name] = v } + mnts := make(map[types.ACName]types.ACName) + for _, m := range ra.Mounts { + mnts[m.MountPoint] = m.Volume + } + for _, mp := range app.MountPoints { - key := mp.Name + key, ok := mnts[mp.Name] + if !ok { + return nil, fmt.Errorf("no mount for mountpoint %q in app %q", mp.Name, name) + } vol, ok := vols[key] if !ok { - return nil, fmt.Errorf("no volume for mountpoint %q in app %q", key, name) + return nil, fmt.Errorf("no volume for mount %q:%q in app %q", mp.Name, key, name) } opt := make([]string, 4) diff --git a/stage1/init/pod_test.go b/stage1/init/pod_test.go index 924a94f07e..e56e786fa7 100644 --- a/stage1/init/pod_test.go +++ b/stage1/init/pod_test.go @@ -115,9 +115,17 @@ func TestAppToNspawnArgsOverridesImageManifestReadOnly(t *testing.T) { }, }, } + appManifest := &schema.RuntimeApp{ + Mounts: []schema.Mount{ + { + Volume: "foo-mount", + MountPoint: "foo-mount", + }, + }, + } p := &Pod{Manifest: podManifest} - output, err := p.appToNspawnArgs(&schema.RuntimeApp{}, imageManifest) + output, err := p.appToNspawnArgs(appManifest, imageManifest) if err != nil { t.Errorf("#%d: unexpected error: `%v`", i, err) } diff --git a/tests/rkt_volume_test.go b/tests/rkt_volume_test.go index c07d5304ce..050c0a43dc 100644 --- a/tests/rkt_volume_test.go +++ b/tests/rkt_volume_test.go @@ -36,11 +36,11 @@ var volTests = []struct { }, // Check that we can read files from a volume (both ro and rw) { - `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR ./rkt-inspect-vol-rw-read-file.aci"`, + `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR --mount=volume=dir1,target=dir1 ./rkt-inspect-vol-rw-read-file.aci"`, `<<>>`, }, { - `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR ./rkt-inspect-vol-ro-read-file.aci"`, + `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR --mount=volume=dir1,target=dir1 ./rkt-inspect-vol-ro-read-file.aci"`, `<<>>`, }, // Check that we can write to files in the ACI @@ -50,16 +50,16 @@ var volTests = []struct { }, // Check that we can write files to a volume (both ro and rw) { - `/bin/sh -c "export FILE=/dir1/file CONTENT=2 ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR ./rkt-inspect-vol-rw-write-file.aci"`, + `/bin/sh -c "export FILE=/dir1/file CONTENT=2 ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR --mount=volume=dir1,target=dir1 ./rkt-inspect-vol-rw-write-file.aci"`, `<<<2>>>`, }, { - `/bin/sh -c "export FILE=/dir1/file CONTENT=3 ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR ./rkt-inspect-vol-ro-write-file.aci"`, + `/bin/sh -c "export FILE=/dir1/file CONTENT=3 ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR --mount=volume=dir1,target=dir1 ./rkt-inspect-vol-ro-write-file.aci"`, `Cannot write to file "/dir1/file": open /dir1/file: read-only file system`, }, // Check that the volume still contain the file previously written { - `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR ./rkt-inspect-vol-ro-read-file.aci"`, + `/bin/sh -c "export FILE=/dir1/file ; ../bin/rkt --debug --insecure-skip-verify run --inherit-env=true --volume=dir1,kind=host,source=$TMPDIR --mount=volume=dir1,target=dir1 ./rkt-inspect-vol-ro-read-file.aci"`, `<<<2>>>`, }, } From d2e57589838f18950335431657dc165b8ffff9d7 Mon Sep 17 00:00:00 2001 From: Vito Caputo Date: Wed, 15 Apr 2015 08:41:04 -0700 Subject: [PATCH 2/2] rkt: add validation of cli apps Currently only checks for volume<->mount connectivity. --- common/apps/apps.go | 31 +++++++++++++++++++++++++++++++ rkt/cli_apps.go | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/common/apps/apps.go b/common/apps/apps.go index 9e68c30b54..9e42a6e448 100644 --- a/common/apps/apps.go +++ b/common/apps/apps.go @@ -17,6 +17,8 @@ package apps import ( + "fmt" + "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema" "github.com/coreos/rkt/Godeps/_workspace/src/github.com/appc/spec/schema/types" ) @@ -60,6 +62,35 @@ func (al *Apps) Last() *App { return &al.apps[len(al.apps)-1] } +// Validate validates al for things like referential integrity of mounts<->volumes. +func (al *Apps) Validate() error { + vs := map[types.ACName]struct{}{} + for _, v := range al.Volumes { + vs[v.Name] = struct{}{} + } + + f := func(mnts []schema.Mount) error { + for _, m := range mnts { + _, ok := vs[m.Volume] + if !ok { + return fmt.Errorf("dangling mount point %q: volume %q not found", m.MountPoint, m.Volume) + } + } + return nil + } + + if err := f(al.Mounts); err != nil { + return err + } + + err := al.Walk(func(app *App) error { + return f(app.Mounts) + }) + + /* TODO(vc): in debug/verbose mode say something about unused volumes? */ + return err +} + // Walk iterates on al.apps calling f for each app // walking stops if f returns an error, the error is simply returned func (al *Apps) Walk(f func(*App) error) error { diff --git a/rkt/cli_apps.go b/rkt/cli_apps.go index 4c5d2ed0d0..f97d877063 100644 --- a/rkt/cli_apps.go +++ b/rkt/cli_apps.go @@ -101,7 +101,7 @@ func parseApps(al *apps.Apps, args []string, flags *flag.FlagSet, allowAppArgs b } } - return nil + return al.Validate() } // Value interface implementations for the various per-app fields we provide flags for