Skip to content

Commit

Permalink
Add feature to mount host volumes (#2949)
Browse files Browse the repository at this point in the history
* Add mount host volume action argument

- Fixes #475
- Add option to mount host volume

---------

Signed-off-by: Maninderjit Bindra <maninder.bindra@gmail.com>
  • Loading branch information
maniSbindra authored Feb 7, 2024
1 parent 07ad7bb commit 35d7997
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 12 deletions.
1 change: 1 addition & 0 deletions cmd/porter/installations.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ func addBundleActionFlags(f *pflag.FlagSet, actionOpts porter.BundleAction) {
addBundlePullFlags(f, &opts.BundlePullOptions)
f.BoolVar(&opts.AllowDockerHostAccess, "allow-docker-host-access", false,
"Controls if the bundle should have access to the host's Docker daemon with elevated privileges. See https://getporter.org/configuration/#allow-docker-host-access for the full implications of this flag.")
f.StringArrayVar(&opts.HostVolumeMounts, "mount-host-volume", nil, "Mount a host volume into the bundle. Format is <host path>:<container path>[:<option>]. May be specified multiple times. Option can be ro (read-only), rw (read-write), default is ro.")
f.BoolVar(&opts.NoLogs, "no-logs", false,
"Do not persist the bundle execution logs")
f.StringArrayVarP(&opts.ParameterSets, "parameter-set", "p", nil,
Expand Down
9 changes: 9 additions & 0 deletions pkg/cnab/provider/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import (
"go.uber.org/zap/zapcore"
)

type HostVolumeMountSpec struct {
Source string
Target string
ReadOnly bool
}

// Shared arguments for all CNAB actions
type ActionArguments struct {
// Action to execute, e.g. install, upgrade.
Expand All @@ -42,6 +48,9 @@ type ActionArguments struct {
// Give the bundle privileged access to the docker daemon.
AllowDockerHostAccess bool

// MountHostVolumes is a map of host paths to container paths to mount.
HostVolumeMounts []HostVolumeMountSpec

// PersistLogs specifies if the invocation image output should be saved as an output.
PersistLogs bool
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/cnab/provider/docker_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ func (r *Runtime) mountDockerSocket(cfg *container.Config, hostCfg *container.Ho
hostCfg.Mounts = append(hostCfg.Mounts, dockerSockMount)
return nil
}

func (r *Runtime) addVolumeMountToHostConfig(hostConfig *container.HostConfig, source string, target string, readOnly bool) error {
mount := mount.Mount{
Source: source,
Target: target,
Type: "bind",
ReadOnly: readOnly,
}
hostConfig.Mounts = append(hostConfig.Mounts, mount)
return nil
}
65 changes: 53 additions & 12 deletions pkg/cnab/provider/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,55 @@ func (r *Runtime) newDriver(driverName string, args ActionArguments) (driver.Dri
return nil, err
}

if args.AllowDockerHostAccess {
if driverName != DriverNameDocker {
return nil, fmt.Errorf("allow-docker-host-access was enabled, but the driver is %s", driverName)
}
if args.AllowDockerHostAccess && driverName != DriverNameDocker {
return nil, fmt.Errorf("allow-docker-host-access was enabled, but the driver is %s", driverName)
}

driverImpl, err = r.dockerDriverWithHostAccess(dockerExt)
} else {
if dockerRequired {
return nil, fmt.Errorf("extension %q is required but allow-docker-host-access was not enabled",
cnab.DockerExtensionKey)
}
if dockerRequired && !args.AllowDockerHostAccess {
return nil, fmt.Errorf("extension %q is required but allow-docker-host-access was not enabled", cnab.DockerExtensionKey)
}

if len(args.HostVolumeMounts) > 0 && driverName != DriverNameDocker {
return nil, fmt.Errorf("mount-host-volume was was used to mount a volume, but the driver is %s", driverName)
}

if !args.AllowDockerHostAccess && len(args.HostVolumeMounts) == 0 {
driverImpl, err = drivers.LookupDriver(r.Context, driverName)
}
if err != nil {
return nil, err
}

var d *docker.Driver
if args.AllowDockerHostAccess || len(args.HostVolumeMounts) > 0 {
d = &docker.Driver{}
}

if args.AllowDockerHostAccess {
driverImpl, err = r.dockerDriverWithHostAccess(dockerExt, d)
}
if err != nil {
return nil, err
}

if len(args.HostVolumeMounts) > 0 {
driverImpl, err = func(dr *docker.Driver) (driver.Driver, error) {

dr.AddConfigurationOptions(func(cfg *container.Config, hostCfg *container.HostConfig) error {
err := r.addVolumeMountsToHostConfig(hostCfg, args.HostVolumeMounts)
if err != nil {
return err
}
return nil
})

return driver.Driver(dr), nil
}(d)
if err != nil {
return nil, err
}
}

if configurable, ok := driverImpl.(driver.Configurable); ok {
driverCfg := make(map[string]string)
// Load any driver-specific config out of the environment
Expand All @@ -60,8 +92,7 @@ func (r *Runtime) newDriver(driverName string, args ActionArguments) (driver.Dri
return driverImpl, nil
}

func (r *Runtime) dockerDriverWithHostAccess(config cnab.Docker) (driver.Driver, error) {
d := &docker.Driver{}
func (r *Runtime) dockerDriverWithHostAccess(config cnab.Docker, d *docker.Driver) (driver.Driver, error) {

// Run the container with privileged access if necessary
if config.Privileged {
Expand All @@ -78,3 +109,13 @@ func (r *Runtime) dockerDriverWithHostAccess(config cnab.Docker) (driver.Driver,

return driver.Driver(d), nil
}

func (r *Runtime) addVolumeMountsToHostConfig(hostConfig *container.HostConfig, mounts []HostVolumeMountSpec) error {
for _, mount := range mounts {
err := r.addVolumeMountToHostConfig(hostConfig, mount.Source, mount.Target, mount.ReadOnly)
if err != nil {
return err
}
}
return nil
}
11 changes: 11 additions & 0 deletions pkg/cnab/provider/driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ func (r *Runtime) mountDockerSocket(cfg *container.Config, hostCfg *container.Ho
hostCfg.Mounts = append(hostCfg.Mounts, dockerSockMount)
return nil
}

func (r *Runtime) addVolumeMountToHostConfig(hostConfig *container.HostConfig, source string, target string, readOnly bool) error {
mount := mount.Mount{
Source: source,
Target: target,
Type: "bind",
ReadOnly: readOnly,
}
hostConfig.Mounts = append(hostConfig.Mounts, mount)
return nil
}
173 changes: 173 additions & 0 deletions pkg/cnab/provider/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,177 @@ func TestNewDriver_Docker(t *testing.T) {
require.NoError(t, err)
require.Equal(t, true, containerHostCfg.Privileged)
})

t.Run("docker with host access, default config, and host volume mounts", func(t *testing.T) {
t.Parallel()

r := NewTestRuntime(t)
r.MockGetDockerGroupId()
defer r.Close()

r.Extensions[cnab.DockerExtensionKey] = cnab.Docker{}
r.FileSystem.Create("/var/run/docker.sock")
r.FileSystem.Create("/sourceFolder")
r.FileSystem.Create("/sourceFolder2")
r.FileSystem.Create("/sourceFolder3")

args := ActionArguments{
AllowDockerHostAccess: true,
HostVolumeMounts: []HostVolumeMountSpec{
{
Source: "/sourceFolder",
Target: "/targetFolder",
},
{
Source: "/sourceFolder2",
Target: "/targetFolder2",
ReadOnly: true,
},
{
Source: "/sourceFolder3",
Target: "/targetFolder3",
ReadOnly: false,
},
},
}

driver, err := r.newDriver(DriverNameDocker, args)
require.NoError(t, err)
assert.IsType(t, driver, &docker.Driver{})

dockerish, ok := driver.(*docker.Driver)
assert.True(t, ok)

err = dockerish.ApplyConfigurationOptions()
assert.NoError(t, err)

containerHostCfg, err := dockerish.GetContainerHostConfig()
require.NoError(t, err)
require.Equal(t, false, containerHostCfg.Privileged)

require.Len(t, containerHostCfg.Mounts, 4) //includes the docker socket mount
assert.Equal(t, "/sourceFolder", containerHostCfg.Mounts[1].Source)
assert.Equal(t, "/targetFolder", containerHostCfg.Mounts[1].Target)
assert.Equal(t, false, containerHostCfg.Mounts[1].ReadOnly)
assert.Equal(t, "/sourceFolder2", containerHostCfg.Mounts[2].Source)
assert.Equal(t, "/targetFolder2", containerHostCfg.Mounts[2].Target)
assert.Equal(t, true, containerHostCfg.Mounts[2].ReadOnly)
assert.Equal(t, "/sourceFolder3", containerHostCfg.Mounts[3].Source)
assert.Equal(t, "/targetFolder3", containerHostCfg.Mounts[3].Target)
assert.Equal(t, false, containerHostCfg.Mounts[3].ReadOnly)
})

t.Run("host volume mount, docker driver, with multiple mounts", func(t *testing.T) {
t.Parallel()

r := NewTestRuntime(t)
defer r.Close()

r.FileSystem.Create("/sourceFolder")
r.FileSystem.Create("/sourceFolder2")
r.FileSystem.Create("/sourceFolder3")

args := ActionArguments{
HostVolumeMounts: []HostVolumeMountSpec{
{
Source: "/sourceFolder",
Target: "/targetFolder",
},
{
Source: "/sourceFolder2",
Target: "/targetFolder2",
ReadOnly: true,
},
{
Source: "/sourceFolder3",
Target: "/targetFolder3",
ReadOnly: false,
},
},
}

driver, err := r.newDriver(DriverNameDocker, args)

require.NoError(t, err)
assert.IsType(t, driver, &docker.Driver{})

dockerish, ok := driver.(*docker.Driver)
assert.True(t, ok)

err = dockerish.ApplyConfigurationOptions()
assert.NoError(t, err)

containerHostCfg, err := dockerish.GetContainerHostConfig()
require.NoError(t, err)

require.Len(t, containerHostCfg.Mounts, 3)
assert.Equal(t, "/sourceFolder", containerHostCfg.Mounts[0].Source)
assert.Equal(t, "/targetFolder", containerHostCfg.Mounts[0].Target)
assert.Equal(t, false, containerHostCfg.Mounts[0].ReadOnly)
assert.Equal(t, "/sourceFolder2", containerHostCfg.Mounts[1].Source)
assert.Equal(t, "/targetFolder2", containerHostCfg.Mounts[1].Target)
assert.Equal(t, true, containerHostCfg.Mounts[1].ReadOnly)
assert.Equal(t, "/sourceFolder3", containerHostCfg.Mounts[2].Source)
assert.Equal(t, "/targetFolder3", containerHostCfg.Mounts[2].Target)
assert.Equal(t, false, containerHostCfg.Mounts[2].ReadOnly)

})

t.Run("host volume mount, docker driver, with single mount", func(t *testing.T) {
t.Parallel()

r := NewTestRuntime(t)
defer r.Close()

r.FileSystem.Create("/sourceFolder")

args := ActionArguments{
HostVolumeMounts: []HostVolumeMountSpec{
{
Source: "/sourceFolder",
Target: "/targetFolder",
},
},
}

driver, err := r.newDriver(DriverNameDocker, args)

require.NoError(t, err)
assert.IsType(t, driver, &docker.Driver{})

dockerish, ok := driver.(*docker.Driver)
assert.True(t, ok)

err = dockerish.ApplyConfigurationOptions()
assert.NoError(t, err)

containerHostCfg, err := dockerish.GetContainerHostConfig()
require.NoError(t, err)

require.Len(t, containerHostCfg.Mounts, 1)
assert.Equal(t, "/sourceFolder", containerHostCfg.Mounts[0].Source)
assert.Equal(t, "/targetFolder", containerHostCfg.Mounts[0].Target)
assert.Equal(t, false, containerHostCfg.Mounts[0].ReadOnly)
})

t.Run("host volume mount, mismatch driver name", func(t *testing.T) {
t.Parallel()

r := NewTestRuntime(t)
r.MockGetDockerGroupId()
defer r.Close()

args := ActionArguments{
HostVolumeMounts: []HostVolumeMountSpec{
{
Source: "/sourceFolder",
Target: "/targetFolder",
},
},
}

_, err := r.newDriver("custom-driver", args)

assert.EqualError(t, err, "mount-host-volume was was used to mount a volume, but the driver is custom-driver")
})
}
11 changes: 11 additions & 0 deletions pkg/cnab/provider/driver_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ func (r *Runtime) mountDockerSocket(cfg *container.Config, hostCfg *container.Ho
hostCfg.Mounts = append(hostCfg.Mounts, dockerSockMount)
return nil
}

func (r *Runtime) addVolumeMountToHostConfig(hostConfig *container.HostConfig, source string, target string, readOnly bool) error {
mount := mount.Mount{
Source: source,
Target: target,
Type: "bind",
ReadOnly: readOnly,
}
hostConfig.Mounts = append(hostConfig.Mounts, mount)
return nil
}
Loading

0 comments on commit 35d7997

Please sign in to comment.