Merge pull request #8264 from dokku/6814-rewrite-storage-plugin-in-golang

Rewrite the storage plugin in golang
This commit is contained in:
Jose Diaz-Gonzalez
2026-01-08 12:45:52 -05:00
committed by GitHub
27 changed files with 915 additions and 375 deletions

View File

@@ -88,6 +88,29 @@ func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) {
return options, nil
}
// RemoveDockerOptionFromPhases removes a docker option from specified phases
func RemoveDockerOptionFromPhases(appName string, phases []string, option string) error {
for _, phase := range phases {
options, err := GetDockerOptionsForPhase(appName, phase)
if err != nil {
return err
}
newOptions := []string{}
for _, opt := range options {
if opt != option {
newOptions = append(newOptions, opt)
}
}
sort.Strings(newOptions)
if err = writeDockerOptionsForPhase(appName, phase, newOptions); err != nil {
return err
}
}
return nil
}
// GetSpecifiedDockerOptionsForPhase returns the docker options for the specified phase that are in the desiredOptions list
// It expects desiredOptions to be a list of docker options that are in the format "--option"
// And will retrieve any lines that start with the desired option

8
plugins/storage/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/commands
/subcommands/*
/triggers/*
/triggers
/storage-*
/install
/post-*
/report

7
plugins/storage/Makefile Normal file
View File

@@ -0,0 +1,7 @@
GOARCH ?= amd64
SUBCOMMANDS = subcommands/default subcommands/ensure-directory subcommands/list subcommands/mount subcommands/report subcommands/unmount
TRIGGERS = triggers/install triggers/storage-list
BUILD = commands subcommands triggers
PLUGIN_NAME = storage
include ../../common.mk

View File

@@ -16,10 +16,13 @@ main() {
exit 1
fi
if [[ "$CHOWN_ID" != "32767" ]] && [[ "$CHOWN_ID" != "1000" ]] && [[ "$CHOWN_ID" != "2000" ]]; then
echo " ! Unsupported chown permissions. Supported values: 32767, 1000, 2000"
exit 1
fi
case "$CHOWN_ID" in
0 | 1000 | 2000 | 32767 | 165536 | 166536 | 167536 | 198303) ;;
*)
echo " ! Unsupported chown permissions. Supported values: 0, 1000, 2000, 32767 (and user namespace offset variants)" 1>&2
exit 1
;;
esac
chown -R "$CHOWN_ID:$CHOWN_ID" "${DOKKU_LIB_ROOT}/data/storage/$DIRECTORY"
}

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
[[ " storage:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
source "$PLUGIN_AVAILABLE_PATH/storage/help-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
case "$1" in
help | storage:help)
cmd-storage-help "$@"
;;
*)
exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
;;
esac

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
verify_paths() {
declare desc="verifies storage paths"
local -r passed_path=$1
if [[ "$passed_path" == /* ]]; then
echo "$passed_path" | grep -qe '^/.*\:/' || dokku_log_fail "Storage path must be two valid paths divided by colon."
else
echo "$passed_path" | grep -qe '^[a-zA-Z0-9]\{1\}[a-zA-Z0-9_.-]\+\:\/' || dokku_log_fail "Volume name must be two characters or more. Volume name must not contain invalid characters. Storage path must be two valid paths divided by colon."
fi
}
check_if_path_exists() {
declare desc="checks if path exists"
local -r passed_path=$1
local -r phase_file_path=$2
[[ -r "$phase_file_path" ]] && grep -qe "^-v $passed_path" "$phase_file_path"
}
get_bind_mounts() {
declare desc="strips docker options and prints mounts"
local -r phase_file_path=$1
if [[ -r "$phase_file_path" ]]; then
sed -e '/^-v/!d' -e 's/^-v //' <"$phase_file_path"
fi
}

37
plugins/storage/go.mod Normal file
View File

@@ -0,0 +1,37 @@
module github.com/dokku/dokku/plugins/storage
go 1.25.5
require (
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000
github.com/onsi/gomega v1.38.3
github.com/spf13/pflag v1.0.10
)
require (
github.com/alexellis/go-execute/v2 v2.2.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/melbahja/goph v1.4.0 // indirect
github.com/otiai10/copy v1.14.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
)
replace github.com/dokku/dokku/plugins/common => ../common
replace github.com/dokku/dokku/plugins/docker-options => ../docker-options

108
plugins/storage/go.sum Normal file
View File

@@ -0,0 +1,108 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alexellis/go-execute/v2 v2.2.1 h1:4Ye3jiCKQarstODOEmqDSRCqxMHLkC92Bhse743RdOI=
github.com/alexellis/go-execute/v2 v2.2.1/go.mod h1:FMdRnUTiFAmYXcv23txrp3VYZfLo24nMpiIneWgKHTQ=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs=
github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
github.com/onsi/ginkgo/v2 v2.25.3/go.mod h1:43uiyQC4Ed2tkOzLsEYm7hnrb7UJTWHYNsuy3bG/snE=
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk=
github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-storage-help() {
declare desc="help command"
declare CMD="$1"
local plugin_name="storage"
local plugin_description="Manage mounted volumes"
if [[ "$CMD" == "${plugin_name}:help" ]]; then
echo -e "Usage: dokku ${plugin_name}[:COMMAND]"
echo ''
echo "$plugin_description"
echo ''
echo 'Additional commands:'
fn-help-content | sort | column -c2 -t -s,
elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then
fn-help-content
else
cat <<help_desc
$plugin_name, $plugin_description
help_desc
fi
}
fn-help-content() {
declare desc="return help content"
cat <<help_content
storage:ensure-directory [--chown option] <directory>, Creates a persistent storage directory in the recommended storage path
storage:list <app> [--format text|json], List bind mounts for app's container(s) (host:container)
storage:mount <app> <host-dir:container-dir>, Create a new bind mount
storage:report [<app>] [<flag>], Displays a checks report for one or more apps
storage:unmount <app> <host-dir:container-dir>, Remove an existing bind mount
help_content
}

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
trigger-storage-install() {
declare desc="storage install trigger"
declare trigger="install"
mkdir -p "${DOKKU_LIB_ROOT}/data/storage"
chown "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/storage"
STORAGE_SUDOERS_FILE="/etc/sudoers.d/dokku-storage"
local mode="0440"
case "$DOKKU_DISTRO" in
arch | debian | raspbian | ubuntu)
echo "%dokku ALL=(ALL) NOPASSWD:$PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir *" >"$STORAGE_SUDOERS_FILE"
echo "Defaults env_keep += \"DOKKU_LIB_ROOT\"" >>"$STORAGE_SUDOERS_FILE"
;;
esac
chmod "$mode" "$STORAGE_SUDOERS_FILE"
}
trigger-storage-install "$@"

View File

@@ -1,168 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/storage/functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-storage-ensure-directory() {
declare desc="creates a persistent storage directory in the recommended storage path"
declare cmd="storage:ensure-directory"
[[ "$1" == "$cmd" ]] && shift 1
declare DIRECTORY CHOWN_FLAG
CHOWN_FLAG=herokuish
skip=false
for arg in "$@"; do
if [[ "$arg" == "--chown" ]]; then
skip=true
continue
fi
if [[ "$skip" == "true" ]]; then
CHOWN_FLAG="$arg"
skip=false
continue
fi
if [[ -z "$DIRECTORY" ]]; then
DIRECTORY="$arg"
fi
done
if [[ -z "$DIRECTORY" ]]; then
dokku_log_fail "Please specify a directory to create"
fi
if [[ ! "$DIRECTORY" =~ ^[A-Za-z0-9\\_-]+$ ]]; then
dokku_log_fail "Directory can only contain the following set of characters: [A-Za-z0-9_-]"
fi
if [[ "$CHOWN_FLAG" == "herokuish" ]]; then
CHOWN_FLAG="32767"
elif [[ "$CHOWN_FLAG" == "heroku" ]]; then
CHOWN_FLAG="1000"
elif [[ "$CHOWN_FLAG" == "packeto" ]]; then
CHOWN_FLAG="2000"
dokku_log_verbose "Detected deprecated chown flag 'packeto'. Using 'paketo' instead. Please update your configuration."
elif [[ "$CHOWN_FLAG" == "paketo" ]]; then
CHOWN_FLAG="2000"
elif [[ "$CHOWN_FLAG" == "root" ]]; then
CHOWN_FLAG="0"
elif [[ "$CHOWN_FLAG" == "false" ]]; then
CHOWN_FLAG="false"
else
dokku_log_fail "Unsupported chown permissions"
fi
userns_enabled="$(docker info -f '{{range .SecurityOptions}}{{if eq . "name=userns"}}true{{end}}{{end}}')"
if [[ "$userns_enabled" == "true" ]] && [[ "$CHOWN_FLAG" != "false" ]]; then
CHOWN_FLAG=$((CHOWN_FLAG + 165536))
fi
local storage_directory="${DOKKU_LIB_ROOT}/data/storage/$DIRECTORY"
dokku_log_info1 "Ensuring ${storage_directory} exists"
mkdir -p "${storage_directory}"
if [[ "$CHOWN_FLAG" != "false" ]]; then
dokku_log_verbose_quiet "Setting directory ownership to $CHOWN_FLAG:$CHOWN_FLAG"
sudo "$PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir" "$DIRECTORY" "$CHOWN_FLAG"
fi
dokku_log_verbose_quiet "Directory ready for mounting"
}
cmd-storage-report() {
declare desc="displays a storage report for one or more apps"
declare cmd="storage:report"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" INFO_FLAG="$2"
if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then
INFO_FLAG="$APP"
APP=""
fi
if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then
INFO_FLAG="true"
fi
if [[ -z "$APP" ]]; then
for app in $(dokku_apps); do
cmd-storage-report-single "$app" "$INFO_FLAG" | tee || true
done
else
cmd-storage-report-single "$APP" "$INFO_FLAG"
fi
}
cmd-storage-report-single() {
declare APP="$1" INFO_FLAG="$2"
if [[ "$INFO_FLAG" == "true" ]]; then
INFO_FLAG=""
fi
verify_app_name "$APP"
local flag_map=(
"--storage-build-mounts: $(fn-storage-bind-mounts "$APP" build)"
"--storage-deploy-mounts: $(fn-storage-bind-mounts "$APP" deploy)"
"--storage-run-mounts: $(fn-storage-bind-mounts "$APP" run)"
)
if [[ -z "$INFO_FLAG" ]]; then
dokku_log_info2_quiet "$APP storage information"
for flag in "${flag_map[@]}"; do
key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')"
dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")"
done
else
local match=false
local value_exists=false
for flag in "${flag_map[@]}"; do
valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)"
if [[ "$flag" == "${INFO_FLAG}:"* ]]; then
value=${flag#*: }
size="${#value}"
if [[ "$size" -ne 0 ]]; then
echo "$value" && match=true && value_exists=true
else
match=true
fi
fi
done
[[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}"
[[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed"
fi
}
fn-storage-bind-mounts() {
declare APP="$1" PHASE="$2"
local PHASE_FILE="$(fn-get-phase-file-path "$APP" "$PHASE")"
if [[ -r "$PHASE_FILE" ]]; then
sed -e '/^-v/!d' "$PHASE_FILE" | tr '\n' ' '
fi
}
cmd-storage-list() {
declare desc="List all bound mounts"
declare cmd="storage:list"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" FORMAT_FLAG="$2" FORMAT_FLAG_VALUE="$3"
local phase="deploy" format="text"
if [[ "$FORMAT_FLAG" == "--format" ]]; then
if [[ "$FORMAT_FLAG_VALUE" == "json" ]] || [[ "$FORMAT_FLAG_VALUE" == "text" ]]; then
format="$FORMAT_FLAG_VALUE"
else
dokku_log_fail "Invalid --format value specified"
fi
elif [[ -n "$FORMAT_FLAG" ]]; then
dokku_log_fail "Invalid argument specified"
fi
verify_app_name "$APP"
if [[ "$FORMAT_FLAG_VALUE" == "text" ]]; then
dokku_log_info1_quiet "$APP volume bind-mounts:"
plugn trigger storage-list "$APP" "$phase" "$format" | sed "s/^/ /"
else
plugn trigger storage-list "$APP" "$phase" "$format"
fi
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_AVAILABLE_PATH/storage/internal-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-storage-report-single "$@"

37
plugins/storage/report.go Normal file
View File

@@ -0,0 +1,37 @@
package storage
import "github.com/dokku/dokku/plugins/common"
func ReportSingleApp(appName string, infoFlag string, format string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
flags := map[string]common.ReportFunc{
"--storage-build-mounts": reportBuildMounts,
"--storage-deploy-mounts": reportDeployMounts,
"--storage-run-mounts": reportRunMounts,
}
flagKeys := []string{}
for flagKey := range flags {
flagKeys = append(flagKeys, flagKey)
}
trimPrefix := false
uppercaseFirstCharacter := true
infoFlags := common.CollectReport(appName, infoFlag, flags)
return common.ReportSingleApp("storage", appName, infoFlag, infoFlags, flagKeys, format, trimPrefix, uppercaseFirstCharacter)
}
func reportBuildMounts(appName string) string {
return GetBindMountsForDisplay(appName, "build")
}
func reportDeployMounts(appName string) string {
return GetBindMountsForDisplay(appName, "deploy")
}
func reportRunMounts(appName string) string {
return GetBindMountsForDisplay(appName, "run")
}

View File

@@ -0,0 +1,60 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
)
const (
helpHeader = `Usage: dokku storage[:COMMAND]
Manage mounted volumes
Additional commands:`
helpContent = `
storage:ensure-directory [--chown option] <directory>, Creates a persistent storage directory in the recommended storage path
storage:list <app> [--format text|json], List bind mounts for app's container(s) (host:container)
storage:mount <app> <host-dir:container-dir>, Create a new bind mount
storage:report [<app>] [<flag>], Displays a storage report for one or more apps
storage:unmount <app> <host-dir:container-dir>, Remove an existing bind mount`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "storage":
usage()
case "storage:help":
usage()
case "help":
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "ps",
Args: []string{"-o", "command=", strconv.Itoa(os.Getppid())},
})
if err == nil && strings.Contains(result.StdoutContents(), "--all") {
fmt.Println(helpContent)
} else {
fmt.Print("\n storage, Manage mounted volumes\n")
}
default:
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
if err != nil {
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
dokkuNotImplementExitCode = 10
}
os.Exit(dokkuNotImplementExitCode)
}
}
func usage() {
common.CommandUsage(helpHeader, helpContent)
}

View File

@@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/storage"
flag "github.com/spf13/pflag"
)
func main() {
parts := strings.Split(os.Args[0], "/")
subcommand := parts[len(parts)-1]
var err error
switch subcommand {
case "default":
err = storage.CommandHelp()
case "ensure-directory":
args := flag.NewFlagSet("storage:ensure-directory", flag.ExitOnError)
chown := args.String("chown", "herokuish", "--chown: chown option (herokuish, heroku, paketo, root, false)")
args.Parse(os.Args[2:])
directory := args.Arg(0)
err = storage.CommandEnsureDirectory(directory, *chown)
case "list":
args := flag.NewFlagSet("storage:list", flag.ExitOnError)
format := args.String("format", "text", "--format: output format (text, json)")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = storage.CommandList(appName, *format)
case "mount":
args := flag.NewFlagSet("storage:mount", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
mountPath := args.Arg(1)
err = storage.CommandMount(appName, mountPath)
case "report":
args := flag.NewFlagSet("scheduler-k3s:report", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
osArgs, infoFlag, flagErr := common.ParseReportArgs("scheduler-k3s", os.Args[2:])
if flagErr == nil {
args.Parse(osArgs)
appName := args.Arg(0)
err = storage.CommandReport(appName, *format, infoFlag)
}
case "unmount":
args := flag.NewFlagSet("storage:unmount", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
mountPath := args.Arg(1)
err = storage.CommandUnmount(appName, mountPath)
default:
err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand)
}
if err != nil {
common.LogFailWithError(err)
}
}

View File

@@ -0,0 +1,34 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/storage"
)
func main() {
parts := strings.Split(os.Args[0], "/")
trigger := parts[len(parts)-1]
flag.Parse()
var err error
switch trigger {
case "install":
err = storage.TriggerInstall()
case "storage-list":
appName := flag.Arg(0)
phase := flag.Arg(1)
format := flag.Arg(2)
err = storage.TriggerStorageList(appName, phase, format)
default:
err = fmt.Errorf("Invalid plugin trigger call: %s", trigger)
}
if err != nil {
common.LogFailWithError(err)
}
}

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/storage/functions"
trigger-storage-storage-list() {
declare desc="storage storage-list trigger"
declare trigger="storage-list"
declare APP="$1" PHASE="$2" FORMAT="$3"
if [[ "$FORMAT" != "json" ]]; then
get_bind_mounts "$(fn-get-phase-file-path "$APP" "$PHASE")"
else
while read -r line; do
local host_path="$(awk -F: '{print $1}' <<<"$line")"
local container_path="$(awk -F: '{print $2}' <<<"$line")"
local volume_options="$(awk -F: '{print $3}' <<<"$line")"
jq -n --arg host_path "$host_path" --arg container_path "$container_path" --arg volume_options "$volume_options" '{host_path: $host_path, container_path: $container_path, volume_options: $volume_options}'
done < <(get_bind_mounts "$(fn-get-phase-file-path "$APP" "$PHASE")") | jq -M -n '[inputs]'
fi
}
trigger-storage-storage-list "$@"

130
plugins/storage/storage.go Normal file
View File

@@ -0,0 +1,130 @@
package storage
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
// MountPhases are the phases where storage mounts are applied
var MountPhases = []string{"deploy", "run"}
// VerifyPaths validates the storage mount path format
func VerifyPaths(mountPath string) error {
if strings.HasPrefix(mountPath, "/") {
matched, err := regexp.MatchString(`^/.*:/`, mountPath)
if err != nil {
return err
}
if !matched {
return errors.New("Storage path must be two valid paths divided by colon.")
}
} else {
matched, err := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9_.-]+:/`, mountPath)
if err != nil {
return err
}
if !matched {
return errors.New("Volume name must be two characters or more. Volume name must not contain invalid characters. Storage path must be two valid paths divided by colon.")
}
}
return nil
}
// CheckIfPathExists checks if a mount path exists in the specified phases
func CheckIfPathExists(appName string, mountPath string, phases []string) bool {
for _, phase := range phases {
options, err := dockeroptions.GetDockerOptionsForPhase(appName, phase)
if err != nil {
continue
}
for _, option := range options {
if option == fmt.Sprintf("-v %s", mountPath) {
return true
}
}
}
return false
}
// GetBindMounts returns the bind mounts for an app and phase
func GetBindMounts(appName string, phase string) ([]string, error) {
mounts := []string{}
options, err := dockeroptions.GetDockerOptionsForPhase(appName, phase)
if err != nil {
return mounts, err
}
for _, option := range options {
if strings.HasPrefix(option, "-v ") {
mount := strings.TrimPrefix(option, "-v ")
mounts = append(mounts, mount)
}
}
return mounts, nil
}
// GetBindMountsForDisplay returns the bind mounts formatted for display
func GetBindMountsForDisplay(appName string, phase string) string {
mounts, err := GetBindMounts(appName, phase)
if err != nil {
return ""
}
result := []string{}
for _, mount := range mounts {
result = append(result, fmt.Sprintf("-v %s", mount))
}
return strings.Join(result, " ")
}
// StorageListEntry represents a storage mount entry for JSON output
type StorageListEntry struct {
HostPath string `json:"host_path"`
ContainerPath string `json:"container_path"`
VolumeOptions string `json:"volume_options"`
}
// ParseMountPath parses a mount path into its components
func ParseMountPath(mountPath string) StorageListEntry {
parts := strings.SplitN(mountPath, ":", 3)
entry := StorageListEntry{}
if len(parts) >= 1 {
entry.HostPath = parts[0]
}
if len(parts) >= 2 {
entry.ContainerPath = parts[1]
}
if len(parts) >= 3 {
entry.VolumeOptions = parts[2]
}
return entry
}
// GetStorageDirectory returns the storage directory path
func GetStorageDirectory() string {
dokkuLibRoot := common.GetenvWithDefault("DOKKU_LIB_ROOT", "/var/lib/dokku")
return fmt.Sprintf("%s/data/storage", dokkuLibRoot)
}
// ValidateDirectoryName validates a storage directory name
func ValidateDirectoryName(directory string) error {
if directory == "" {
return errors.New("Please specify a directory to create")
}
matched, err := regexp.MatchString(`^[A-Za-z0-9_-]+$`, directory)
if err != nil {
return err
}
if !matched {
return errors.New("Directory can only contain the following set of characters: [A-Za-z0-9_-]")
}
return nil
}

View File

@@ -0,0 +1,90 @@
package storage
import (
"testing"
. "github.com/onsi/gomega"
)
func TestVerifyPathsAbsolutePath(t *testing.T) {
RegisterTestingT(t)
Expect(VerifyPaths("/host/path:/container/path")).To(Succeed())
Expect(VerifyPaths("/var/lib/dokku/data/storage/test:/app/data")).To(Succeed())
}
func TestVerifyPathsNamedVolume(t *testing.T) {
RegisterTestingT(t)
Expect(VerifyPaths("volume_name:/container/path")).To(Succeed())
Expect(VerifyPaths("my-volume:/app/data")).To(Succeed())
Expect(VerifyPaths("my.volume:/app/data")).To(Succeed())
}
func TestVerifyPathsInvalid(t *testing.T) {
RegisterTestingT(t)
err := VerifyPaths("/host/path")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Storage path must be two valid paths divided by colon"))
err = VerifyPaths("a:/container")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Volume name must be two characters or more"))
err = VerifyPaths("-invalid:/container")
Expect(err).To(HaveOccurred())
}
func TestValidateDirectoryName(t *testing.T) {
RegisterTestingT(t)
Expect(ValidateDirectoryName("myapp")).To(Succeed())
Expect(ValidateDirectoryName("my-app")).To(Succeed())
Expect(ValidateDirectoryName("my_app")).To(Succeed())
Expect(ValidateDirectoryName("MyApp123")).To(Succeed())
}
func TestValidateDirectoryNameInvalid(t *testing.T) {
RegisterTestingT(t)
err := ValidateDirectoryName("")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Please specify a directory"))
err = ValidateDirectoryName("@invalid")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Directory can only contain"))
err = ValidateDirectoryName("my/app")
Expect(err).To(HaveOccurred())
err = ValidateDirectoryName("my app")
Expect(err).To(HaveOccurred())
}
func TestParseMountPath(t *testing.T) {
RegisterTestingT(t)
entry := ParseMountPath("/host/path:/container/path")
Expect(entry.HostPath).To(Equal("/host/path"))
Expect(entry.ContainerPath).To(Equal("/container/path"))
Expect(entry.VolumeOptions).To(BeEmpty())
entry = ParseMountPath("/host/path:/container/path:ro")
Expect(entry.HostPath).To(Equal("/host/path"))
Expect(entry.ContainerPath).To(Equal("/container/path"))
Expect(entry.VolumeOptions).To(Equal("ro"))
entry = ParseMountPath("volume_name:/container/path")
Expect(entry.HostPath).To(Equal("volume_name"))
Expect(entry.ContainerPath).To(Equal("/container/path"))
Expect(entry.VolumeOptions).To(BeEmpty())
}
func TestGetStorageDirectory(t *testing.T) {
RegisterTestingT(t)
dir := GetStorageDirectory()
Expect(dir).To(ContainSubstring("data/storage"))
}

View File

@@ -0,0 +1,219 @@
package storage
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
const (
helpHeader = `Usage: dokku storage[:COMMAND]
Manage mounted volumes
Additional commands:`
helpContent = `
storage:ensure-directory [--chown option] <directory>, Creates a persistent storage directory in the recommended storage path
storage:list <app> [--format text|json], List bind mounts for app's container(s) (host:container)
storage:mount <app> <host-dir:container-dir>, Create a new bind mount
storage:report [<app>] [<flag>], Displays a storage report for one or more apps
storage:unmount <app> <host-dir:container-dir>, Remove an existing bind mount`
)
// CommandHelp displays help for the storage plugin
func CommandHelp() error {
common.CommandUsage(helpHeader, helpContent)
return nil
}
// CommandEnsureDirectory creates a persistent storage directory
func CommandEnsureDirectory(directory string, chownFlag string) error {
if err := ValidateDirectoryName(directory); err != nil {
return err
}
chownID, err := resolveChownID(chownFlag)
if err != nil {
return err
}
storageDirectory := filepath.Join(GetStorageDirectory(), directory)
common.LogInfo1(fmt.Sprintf("Ensuring %s exists", storageDirectory))
if err := os.MkdirAll(storageDirectory, 0755); err != nil {
return fmt.Errorf("Unable to create directory: %s", err.Error())
}
if chownID != "false" {
common.LogVerboseQuiet(fmt.Sprintf("Setting directory ownership to %s:%s", chownID, chownID))
pluginPath := common.MustGetEnv("PLUGIN_AVAILABLE_PATH")
chownScript := filepath.Join(pluginPath, "storage", "bin", "chown-storage-dir")
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "sudo",
Args: []string{chownScript, directory, chownID},
})
if err != nil {
return fmt.Errorf("Unable to set directory ownership: %s", err.Error())
}
if result.ExitCode != 0 {
return fmt.Errorf("Unable to set directory ownership: %s", result.StderrContents())
}
}
common.LogVerboseQuiet("Directory ready for mounting")
return nil
}
// resolveChownID converts a chown flag value to a numeric UID
func resolveChownID(chownFlag string) (string, error) {
var chownID string
switch chownFlag {
case "herokuish":
chownID = "32767"
case "heroku":
chownID = "1000"
case "packeto":
common.LogVerbose("Detected deprecated chown flag 'packeto'. Using 'paketo' instead. Please update your configuration.")
chownID = "2000"
case "paketo":
chownID = "2000"
case "root":
chownID = "0"
case "false":
return "false", nil
default:
return "", errors.New("Unsupported chown permissions")
}
userns, err := isUserNamespacesEnabled()
if err != nil {
return "", err
}
if userns && chownID != "false" {
uid := 0
fmt.Sscanf(chownID, "%d", &uid)
uid += 165536
chownID = fmt.Sprintf("%d", uid)
}
return chownID, nil
}
// isUserNamespacesEnabled checks if Docker user namespaces are enabled
func isUserNamespacesEnabled() (bool, error) {
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: common.DockerBin(),
Args: []string{"info", "-f", "{{range .SecurityOptions}}{{if eq . \"name=userns\"}}true{{end}}{{end}}"},
})
if err != nil {
return false, err
}
return strings.TrimSpace(result.StdoutContents()) == "true", nil
}
// CommandMount creates a new bind mount for an app
func CommandMount(appName string, mountPath string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
if err := VerifyPaths(mountPath); err != nil {
return err
}
if CheckIfPathExists(appName, mountPath, MountPhases) {
return errors.New("Mount path already exists.")
}
return dockeroptions.AddDockerOptionToPhases(appName, MountPhases, fmt.Sprintf("-v %s", mountPath))
}
// CommandUnmount removes an existing bind mount from an app
func CommandUnmount(appName string, mountPath string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
if err := VerifyPaths(mountPath); err != nil {
return err
}
if !CheckIfPathExists(appName, mountPath, MountPhases) {
return errors.New("Mount path does not exist.")
}
return dockeroptions.RemoveDockerOptionFromPhases(appName, MountPhases, fmt.Sprintf("-v %s", mountPath))
}
// CommandList lists all bind mounts for an app
func CommandList(appName string, format string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
if format != "text" && format != "json" {
return errors.New("Invalid --format value specified")
}
if format == "text" {
common.LogInfo1Quiet(fmt.Sprintf("%s volume bind-mounts:", appName))
}
results, err := common.CallPlugnTrigger(common.PlugnTriggerInput{
Trigger: "storage-list",
Args: []string{appName, "deploy", format},
})
if err != nil {
return err
}
output := results.StdoutContents()
if format == "text" && output != "" {
lines := strings.Split(strings.TrimSpace(output), "\n")
for _, line := range lines {
if line != "" {
if os.Getenv("DOKKU_QUIET_OUTPUT") != "" {
fmt.Println(line)
} else {
common.LogVerbose(line)
}
}
}
} else if output != "" {
fmt.Println(output)
}
return nil
}
// CommandReport displays a storage report for one or more apps
func CommandReport(appName string, format string, infoFlag string) error {
if appName == "" {
apps, err := common.DokkuApps()
if err != nil {
if errors.Is(err, common.NoAppsExist) {
common.LogWarn(err.Error())
return nil
}
return err
}
for _, app := range apps {
if err := ReportSingleApp(app, format, infoFlag); err != nil {
return err
}
}
return nil
}
return ReportSingleApp(appName, format, infoFlag)
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/storage/help-functions"
cmd-storage-help "storage:help"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/storage/internal-functions"
cmd-storage-ensure-directory "$@"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/storage/internal-functions"
cmd-storage-list "$@"

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/storage/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
cmd-storage-mount() {
declare desc="Add bind-mount, redeploy app if running"
declare cmd="storage:mount"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" MOUNT_PATH="$2"
local passed_phases=(deploy run)
verify_app_name "$APP"
verify_paths "$MOUNT_PATH"
check_if_path_exists "$MOUNT_PATH" "$(get_phase_file_path "${passed_phases[@]}")" && dokku_log_fail "Mount path already exists."
add_passed_docker_option passed_phases[@] "-v $MOUNT_PATH"
}
cmd-storage-mount "$@"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_AVAILABLE_PATH/storage/internal-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-storage-report "$@"

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/storage/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
cmd-storage-unmount() {
declare desc="Remove bind-mount, restart app if running"
declare cmd="storage:unmount"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" MOUNT_PATH="$2"
local passed_phases=(deploy run)
verify_app_name "$APP"
verify_paths "$MOUNT_PATH"
check_if_path_exists "$MOUNT_PATH" "$(get_phase_file_path "${passed_phases[@]}")" || dokku_log_fail "Mount path does not exist."
remove_passed_docker_option passed_phases[@] "-v $MOUNT_PATH"
}
cmd-storage-unmount "$@"

View File

@@ -0,0 +1,93 @@
package storage
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/dokku/dokku/plugins/common"
)
// TriggerInstall sets up the storage plugin on installation
func TriggerInstall() error {
storageDir := GetStorageDirectory()
if err := os.MkdirAll(storageDir, 0755); err != nil {
return fmt.Errorf("Unable to create storage directory: %s", err.Error())
}
if err := common.SetPermissions(common.SetPermissionInput{
Filename: storageDir,
Mode: 0755,
}); err != nil {
return fmt.Errorf("Unable to set storage directory permissions: %s", err.Error())
}
distro := detectDistro()
if distro == "" {
return nil
}
pluginPath := common.MustGetEnv("PLUGIN_AVAILABLE_PATH")
chownScript := filepath.Join(pluginPath, "storage", "bin", "chown-storage-dir")
sudoersFile := "/etc/sudoers.d/dokku-storage"
content := fmt.Sprintf("%%dokku ALL=(ALL) NOPASSWD:%s *\n", chownScript)
content += "Defaults env_keep += \"DOKKU_LIB_ROOT\"\n"
if err := os.WriteFile(sudoersFile, []byte(content), 0440); err != nil {
return fmt.Errorf("Unable to write sudoers file: %s", err.Error())
}
return nil
}
// detectDistro returns the Linux distribution name
func detectDistro() string {
if runtime.GOOS != "linux" {
return ""
}
distro := os.Getenv("DOKKU_DISTRO")
if distro != "" {
return distro
}
if common.FileExists("/etc/debian_version") {
return "debian"
}
if common.FileExists("/etc/arch-release") {
return "arch"
}
return ""
}
// TriggerStorageList outputs storage mounts for an app
func TriggerStorageList(appName string, phase string, format string) error {
mounts, err := GetBindMounts(appName, phase)
if err != nil {
return err
}
if format == "json" {
entries := []StorageListEntry{}
for _, mount := range mounts {
entries = append(entries, ParseMountPath(mount))
}
output, err := json.Marshal(entries)
if err != nil {
return err
}
fmt.Println(string(output))
} else {
for _, mount := range mounts {
fmt.Println(mount)
}
}
return nil
}