diff --git a/plugins/docker-options/dockeroptions.go b/plugins/docker-options/dockeroptions.go index 00c0fc906..bbe79bbad 100644 --- a/plugins/docker-options/dockeroptions.go +++ b/plugins/docker-options/dockeroptions.go @@ -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 diff --git a/plugins/storage/.gitignore b/plugins/storage/.gitignore new file mode 100644 index 000000000..d3939e0d4 --- /dev/null +++ b/plugins/storage/.gitignore @@ -0,0 +1,8 @@ +/commands +/subcommands/* +/triggers/* +/triggers +/storage-* +/install +/post-* +/report diff --git a/plugins/storage/Makefile b/plugins/storage/Makefile new file mode 100644 index 000000000..754299f43 --- /dev/null +++ b/plugins/storage/Makefile @@ -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 diff --git a/plugins/storage/bin/chown-storage-dir b/plugins/storage/bin/chown-storage-dir index abb6ba032..c5bb21d39 100755 --- a/plugins/storage/bin/chown-storage-dir +++ b/plugins/storage/bin/chown-storage-dir @@ -16,10 +16,14 @@ 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" } diff --git a/plugins/storage/commands b/plugins/storage/commands deleted file mode 100755 index a3a329180..000000000 --- a/plugins/storage/commands +++ /dev/null @@ -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 diff --git a/plugins/storage/functions b/plugins/storage/functions deleted file mode 100755 index 611e92397..000000000 --- a/plugins/storage/functions +++ /dev/null @@ -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 -} diff --git a/plugins/storage/go.mod b/plugins/storage/go.mod new file mode 100644 index 000000000..92d3e62dc --- /dev/null +++ b/plugins/storage/go.mod @@ -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 diff --git a/plugins/storage/go.sum b/plugins/storage/go.sum new file mode 100644 index 000000000..7e02672dd --- /dev/null +++ b/plugins/storage/go.sum @@ -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= diff --git a/plugins/storage/help-functions b/plugins/storage/help-functions deleted file mode 100755 index 14ce66f57..000000000 --- a/plugins/storage/help-functions +++ /dev/null @@ -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 <, Creates a persistent storage directory in the recommended storage path - storage:list [--format text|json], List bind mounts for app's container(s) (host:container) - storage:mount , Create a new bind mount - storage:report [] [], Displays a checks report for one or more apps - storage:unmount , Remove an existing bind mount -help_content -} diff --git a/plugins/storage/install b/plugins/storage/install deleted file mode 100755 index 1af69cb29..000000000 --- a/plugins/storage/install +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/internal-functions b/plugins/storage/internal-functions deleted file mode 100755 index a130b9bcc..000000000 --- a/plugins/storage/internal-functions +++ /dev/null @@ -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 -} diff --git a/plugins/storage/report b/plugins/storage/report deleted file mode 100755 index e2afe3561..000000000 --- a/plugins/storage/report +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/src/commands/commands.go b/plugins/storage/src/commands/commands.go new file mode 100644 index 000000000..8ad0b2de8 --- /dev/null +++ b/plugins/storage/src/commands/commands.go @@ -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] , Creates a persistent storage directory in the recommended storage path + storage:list [--format text|json], List bind mounts for app's container(s) (host:container) + storage:mount , Create a new bind mount + storage:report [] [], Displays a storage report for one or more apps + storage:unmount , 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) +} diff --git a/plugins/storage/src/subcommands/subcommands.go b/plugins/storage/src/subcommands/subcommands.go new file mode 100644 index 000000000..ef13038d8 --- /dev/null +++ b/plugins/storage/src/subcommands/subcommands.go @@ -0,0 +1,72 @@ +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("storage:report", flag.ExitOnError) + format := args.String("format", "stdout", "--format: output format (stdout, json)") + args.Parse(os.Args[2:]) + + osArgs, infoFlag, parseErr := common.ParseReportArgs("storage", args.Args()) + if parseErr != nil { + err = parseErr + } else { + appName := "" + if len(osArgs) > 0 { + appName = osArgs[0] + } + if *format == "stdout" { + err = storage.CommandReport(appName, infoFlag) + } else { + err = storage.CommandReportSingleApp(appName, infoFlag, *format) + } + } + 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) + } +} diff --git a/plugins/storage/src/triggers/triggers.go b/plugins/storage/src/triggers/triggers.go new file mode 100644 index 000000000..3d2dd3ebf --- /dev/null +++ b/plugins/storage/src/triggers/triggers.go @@ -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) + } +} diff --git a/plugins/storage/storage-list b/plugins/storage/storage-list deleted file mode 100755 index aeb177829..000000000 --- a/plugins/storage/storage-list +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/storage.go b/plugins/storage/storage.go new file mode 100644 index 000000000..386cec11d --- /dev/null +++ b/plugins/storage/storage.go @@ -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 +} diff --git a/plugins/storage/storage_test.go b/plugins/storage/storage_test.go new file mode 100644 index 000000000..305680d3c --- /dev/null +++ b/plugins/storage/storage_test.go @@ -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")) +} diff --git a/plugins/storage/subcommands.go b/plugins/storage/subcommands.go new file mode 100644 index 000000000..d54c24bf5 --- /dev/null +++ b/plugins/storage/subcommands.go @@ -0,0 +1,241 @@ +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] , Creates a persistent storage directory in the recommended storage path + storage:list [--format text|json], List bind mounts for app's container(s) (host:container) + storage:mount , Create a new bind mount + storage:report [] [], Displays a storage report for one or more apps + storage:unmount , 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, infoFlag string) error { + if appName == "" { + apps, err := common.DokkuApps() + if err != nil { + return err + } + + for _, app := range apps { + if err := reportSingle(app, infoFlag, "stdout"); err != nil { + return err + } + } + return nil + } + + return reportSingle(appName, infoFlag, "stdout") +} + +// CommandReportSingleApp displays a storage report for a single app with format support +func CommandReportSingleApp(appName string, infoFlag string, format string) error { + return reportSingle(appName, infoFlag, format) +} + +func reportSingle(appName string, infoFlag string, format string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + infoFlags := map[string]string{ + "--storage-build-mounts": GetBindMountsForDisplay(appName, "build"), + "--storage-deploy-mounts": GetBindMountsForDisplay(appName, "deploy"), + "--storage-run-mounts": GetBindMountsForDisplay(appName, "run"), + } + + infoFlagKeys := []string{ + "--storage-build-mounts", + "--storage-deploy-mounts", + "--storage-run-mounts", + } + + return common.ReportSingleApp("storage", appName, infoFlag, infoFlags, infoFlagKeys, format, true, true) +} diff --git a/plugins/storage/subcommands/default b/plugins/storage/subcommands/default deleted file mode 100755 index 1995ae785..000000000 --- a/plugins/storage/subcommands/default +++ /dev/null @@ -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" diff --git a/plugins/storage/subcommands/ensure-directory b/plugins/storage/subcommands/ensure-directory deleted file mode 100755 index bbce7325d..000000000 --- a/plugins/storage/subcommands/ensure-directory +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/subcommands/list b/plugins/storage/subcommands/list deleted file mode 100755 index 337a396e3..000000000 --- a/plugins/storage/subcommands/list +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/subcommands/mount b/plugins/storage/subcommands/mount deleted file mode 100755 index 27ad9a06c..000000000 --- a/plugins/storage/subcommands/mount +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/subcommands/report b/plugins/storage/subcommands/report deleted file mode 100755 index 6787888bb..000000000 --- a/plugins/storage/subcommands/report +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/subcommands/unmount b/plugins/storage/subcommands/unmount deleted file mode 100755 index 941060302..000000000 --- a/plugins/storage/subcommands/unmount +++ /dev/null @@ -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 "$@" diff --git a/plugins/storage/triggers.go b/plugins/storage/triggers.go new file mode 100644 index 000000000..ac2c4d4e0 --- /dev/null +++ b/plugins/storage/triggers.go @@ -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 +}