feat: add --volume-options flag to storage:mount
The named-entry form of `storage:mount` had no way to set the attachment's `volume_options`, even though the legacy `host:container:opts` colon form parses options into the same field and every downstream consumer (the docker-args trigger and the storage:list/storage:report display path) already renders them. Tooling that declaratively re-applies attachments silently dropped options on every re-apply.
This commit is contained in:
@@ -118,6 +118,15 @@ In the first example, Dokku will then mount the shared contents of `/var/lib/dok
|
||||
|
||||
> If the `/storage` path within the container had pre-existing content, the container files will be over-written. This may be an issue for users that create assets at build time but then mount a directory at the same place during runtime. Files are not merged.
|
||||
|
||||
For named storage entries, additional Docker mount options can be passed via `--volume-options`. The value is a comma-separated mount-options string stored verbatim on the attachment and rendered into the `-v` flag at deploy time. This is useful for SELinux labels (`Z`, `z`) or hardening flags (`noexec,nosuid`):
|
||||
|
||||
```shell
|
||||
dokku storage:create node-js-data
|
||||
dokku storage:mount node-js-app node-js-data --container-dir /app/storage --volume-options Z
|
||||
```
|
||||
|
||||
When combined with `--volume-readonly`, the rendered options become `ro,<volume-options>` - for example, `--volume-options noexec,nosuid --volume-readonly` renders as `:ro,noexec,nosuid`.
|
||||
|
||||
Once persistent storage is mounted, the app requires a restart. See the [process scaling documentation](/docs/processes/process-management.md) for more information.
|
||||
|
||||
```shell
|
||||
|
||||
@@ -129,6 +129,48 @@ func TestListAppMountEntriesK3sUsesEntryName(t *testing.T) {
|
||||
Expect(formatStorageListEntry(rows[0])).To(Equal("demo-pvc:/data"))
|
||||
}
|
||||
|
||||
func TestListAppMountEntriesVolumeOptions(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
root := withTempLibRoot(t)
|
||||
|
||||
Expect(SaveEntry(&Entry{
|
||||
Name: "demo-data",
|
||||
Scheduler: SchedulerDockerLocal,
|
||||
HostPath: "/var/lib/dokku/data/storage/demo-data",
|
||||
})).To(Succeed())
|
||||
|
||||
writeAttachmentsFile(t, root, "demo", []*Attachment{
|
||||
{
|
||||
EntryName: "demo-data",
|
||||
ContainerPath: "/data",
|
||||
Phases: []string{PhaseDeploy, PhaseRun},
|
||||
VolumeOptions: "Z",
|
||||
},
|
||||
{
|
||||
EntryName: "demo-data",
|
||||
ContainerPath: "/ro",
|
||||
Phases: []string{PhaseDeploy, PhaseRun},
|
||||
Readonly: true,
|
||||
VolumeOptions: "noexec,nosuid",
|
||||
},
|
||||
})
|
||||
|
||||
rows, err := ListAppMountEntries("demo", PhaseDeploy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(rows).To(HaveLen(2))
|
||||
|
||||
byPath := map[string]StorageListEntry{}
|
||||
for _, row := range rows {
|
||||
byPath[row.ContainerPath] = row
|
||||
}
|
||||
|
||||
Expect(byPath["/data"].VolumeOptions).To(Equal("Z"))
|
||||
Expect(formatStorageListEntry(byPath["/data"])).To(Equal("/var/lib/dokku/data/storage/demo-data:/data:Z"))
|
||||
|
||||
Expect(byPath["/ro"].VolumeOptions).To(Equal("ro,noexec,nosuid"))
|
||||
Expect(formatStorageListEntry(byPath["/ro"])).To(Equal("/var/lib/dokku/data/storage/demo-data:/ro:ro,noexec,nosuid"))
|
||||
}
|
||||
|
||||
func TestListAppMountEntriesPhaseFilter(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
root := withTempLibRoot(t)
|
||||
|
||||
@@ -155,16 +155,18 @@ func main() {
|
||||
subpath := args.String("volume-subpath", "", "--volume-subpath: subpath within the entry")
|
||||
readonly := args.Bool("volume-readonly", false, "--volume-readonly: mount the volume read-only")
|
||||
volumeChown := args.String("volume-chown", "", "--volume-chown: chown option applied at mount time")
|
||||
volumeOptions := args.String("volume-options", "", "--volume-options: comma-separated mount options (e.g. Z, noexec,nosuid)")
|
||||
args.Parse(os.Args[2:])
|
||||
err = storage.CommandMount(storage.CommandMountInput{
|
||||
AppName: args.Arg(0),
|
||||
NameOrPath: args.Arg(1),
|
||||
ContainerDir: *containerDir,
|
||||
Phases: *phases,
|
||||
ProcessType: *processType,
|
||||
Subpath: *subpath,
|
||||
Readonly: *readonly,
|
||||
VolumeChown: *volumeChown,
|
||||
AppName: args.Arg(0),
|
||||
NameOrPath: args.Arg(1),
|
||||
ContainerDir: *containerDir,
|
||||
Phases: *phases,
|
||||
ProcessType: *processType,
|
||||
Subpath: *subpath,
|
||||
Readonly: *readonly,
|
||||
VolumeChown: *volumeChown,
|
||||
VolumeOptions: *volumeOptions,
|
||||
})
|
||||
case "report":
|
||||
args := flag.NewFlagSet("storage:report", flag.ExitOnError)
|
||||
|
||||
@@ -38,3 +38,25 @@ func TestRepairRegistryOwnership(t *testing.T) {
|
||||
Expect(os.RemoveAll(RegistryDirectory())).To(Succeed())
|
||||
Expect(repairRegistryOwnership()).To(Succeed())
|
||||
}
|
||||
|
||||
// TestBuildDockerVFlagVolumeOptions covers the rendering rules in
|
||||
// buildDockerVFlag for the various combinations of Readonly and
|
||||
// VolumeOptions. This is the docker-args path that the running
|
||||
// container actually receives.
|
||||
func TestBuildDockerVFlagVolumeOptions(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
|
||||
entry := &Entry{HostPath: "/host"}
|
||||
|
||||
plain := buildDockerVFlag(entry, &Attachment{ContainerPath: "/container"})
|
||||
Expect(plain).To(Equal("-v /host:/container"))
|
||||
|
||||
opts := buildDockerVFlag(entry, &Attachment{ContainerPath: "/container", VolumeOptions: "Z"})
|
||||
Expect(opts).To(Equal("-v /host:/container:Z"))
|
||||
|
||||
ro := buildDockerVFlag(entry, &Attachment{ContainerPath: "/container", Readonly: true})
|
||||
Expect(ro).To(Equal("-v /host:/container:ro"))
|
||||
|
||||
roOpts := buildDockerVFlag(entry, &Attachment{ContainerPath: "/container", Readonly: true, VolumeOptions: "noexec,nosuid"})
|
||||
Expect(roOpts).To(Equal("-v /host:/container:ro,noexec,nosuid"))
|
||||
}
|
||||
|
||||
@@ -345,6 +345,56 @@ teardown() {
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "(storage:mount) --volume-options sets option on named-entry attachment" {
|
||||
run /bin/bash -c "dokku storage:create rdmtest-opts"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-opts --container-dir /data --volume-options Z"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].volume_options'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "Z"
|
||||
|
||||
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].container_path'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output "/data"
|
||||
|
||||
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains ":Z"
|
||||
|
||||
# combine with --volume-readonly: rendered as ro,<options>
|
||||
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-opts --container-dir /ro --volume-options noexec,nosuid --volume-readonly"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
|
||||
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
assert_output_contains ":ro,noexec,nosuid"
|
||||
|
||||
# cleanup
|
||||
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-opts --container-dir /data"
|
||||
assert_success
|
||||
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-opts --container-dir /ro"
|
||||
assert_success
|
||||
run /bin/bash -c "dokku storage:destroy rdmtest-opts --force"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "(storage) storage:destroy refuses to remove a still-mounted entry" {
|
||||
run /bin/bash -c "dokku storage:create rdmtest-busy"
|
||||
assert_success
|
||||
|
||||
Reference in New Issue
Block a user