Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to mount host volumes #2949

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",
maniSbindra marked this conversation as resolved.
Show resolved Hide resolved
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
Loading