From 8d0c36bad6d099e4efca5ab5a5123dd47cdcaaa8 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 7 Jul 2026 06:43:31 -0400 Subject: [PATCH] feat: port checks and domains :report subcommands to golang The bash :report implementations for the checks and domains plugins are replaced with a compiled golang binary that collects report keys in parallel and marshals json directly. The domains global report header now matches the shared renderer used by every other golang report, and its bats assertion is updated accordingly. --- go.work | 2 + plugins/checks/.gitignore | 4 + plugins/checks/Makefile | 13 ++ plugins/checks/go.mod | 29 ++++ plugins/checks/go.sum | 55 ++++++++ plugins/checks/internal-functions | 89 ------------- plugins/checks/report | 6 - plugins/checks/report.go | 86 ++++++++++++ plugins/checks/src/subcommands/subcommands.go | 40 ++++++ plugins/checks/src/triggers/triggers.go | 31 +++++ plugins/checks/subcommands.go | 29 ++++ plugins/checks/subcommands/report | 6 - plugins/domains/.gitignore | 4 + plugins/domains/Makefile | 13 ++ plugins/domains/go.mod | 29 ++++ plugins/domains/go.sum | 55 ++++++++ plugins/domains/internal-functions | 89 ------------- plugins/domains/report | 6 - plugins/domains/report.go | 124 ++++++++++++++++++ .../domains/src/subcommands/subcommands.go | 40 ++++++ plugins/domains/src/triggers/triggers.go | 31 +++++ plugins/domains/subcommands.go | 29 ++++ plugins/domains/subcommands/report | 6 - tests/unit/domains.bats | 8 +- 24 files changed, 621 insertions(+), 203 deletions(-) create mode 100644 plugins/checks/.gitignore create mode 100644 plugins/checks/Makefile create mode 100644 plugins/checks/go.mod create mode 100644 plugins/checks/go.sum mode change 100755 => 100644 plugins/checks/internal-functions delete mode 100755 plugins/checks/report create mode 100644 plugins/checks/report.go create mode 100644 plugins/checks/src/subcommands/subcommands.go create mode 100644 plugins/checks/src/triggers/triggers.go create mode 100644 plugins/checks/subcommands.go delete mode 100755 plugins/checks/subcommands/report create mode 100644 plugins/domains/.gitignore create mode 100644 plugins/domains/Makefile create mode 100644 plugins/domains/go.mod create mode 100644 plugins/domains/go.sum mode change 100755 => 100644 plugins/domains/internal-functions delete mode 100755 plugins/domains/report create mode 100644 plugins/domains/report.go create mode 100644 plugins/domains/src/subcommands/subcommands.go create mode 100644 plugins/domains/src/triggers/triggers.go create mode 100644 plugins/domains/subcommands.go delete mode 100755 plugins/domains/subcommands/report diff --git a/go.work b/go.work index faac91ae3..afe21daad 100644 --- a/go.work +++ b/go.work @@ -12,10 +12,12 @@ use ( ./plugins/builder-railpack ./plugins/buildpacks ./plugins/caddy-vhosts + ./plugins/checks ./plugins/common ./plugins/config ./plugins/cron ./plugins/docker-options + ./plugins/domains ./plugins/haproxy-vhosts ./plugins/logs ./plugins/network diff --git a/plugins/checks/.gitignore b/plugins/checks/.gitignore new file mode 100644 index 000000000..dac9d7731 --- /dev/null +++ b/plugins/checks/.gitignore @@ -0,0 +1,4 @@ +/report +/report-subcommand +/triggers +/subcommands/report diff --git a/plugins/checks/Makefile b/plugins/checks/Makefile new file mode 100644 index 000000000..0cfaa38e7 --- /dev/null +++ b/plugins/checks/Makefile @@ -0,0 +1,13 @@ +GOARCH ?= amd64 +TRIGGERS = triggers/report +BUILD = report-subcommand triggers +PLUGIN_NAME = checks + +clean-report-subcommand: + rm -rf report-subcommand + +report-subcommand: clean-report-subcommand src/subcommands/subcommands.go + GOARCH=$(GOARCH) go build -mod=readonly -ldflags="-s -w" $(GO_ARGS) -o report-subcommand src/subcommands/subcommands.go + ln -sf ../report-subcommand subcommands/report + +include ../../common.mk diff --git a/plugins/checks/go.mod b/plugins/checks/go.mod new file mode 100644 index 000000000..998259655 --- /dev/null +++ b/plugins/checks/go.mod @@ -0,0 +1,29 @@ +module github.com/dokku/dokku/plugins/checks + +go 1.26.2 + +require ( + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/spf13/pflag v1.0.10 +) + +require ( + github.com/alexellis/go-execute/v2 v2.2.1 // indirect + github.com/fatih/color v1.19.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.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/melbahja/goph v1.5.1 // 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.10 // indirect + github.com/ryanuber/columnize v2.1.2+incompatible // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect +) + +replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/checks/go.sum b/plugins/checks/go.sum new file mode 100644 index 000000000..5a5292407 --- /dev/null +++ b/plugins/checks/go.sum @@ -0,0 +1,55 @@ +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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.5.1 h1:gdZyjn5nM6FO6mAZrv7XqE2gArQbb+V6FdIyH3Ru3vA= +github.com/melbahja/goph v1.5.1/go.mod h1:dDwo+44cmvfDLdiVpc6fJxexf5BA5yEDUeE5YgtuDO4= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +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.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +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/checks/internal-functions b/plugins/checks/internal-functions old mode 100755 new mode 100644 index 9e4dacd84..7ab635962 --- a/plugins/checks/internal-functions +++ b/plugins/checks/internal-functions @@ -5,95 +5,6 @@ source "$PLUGIN_AVAILABLE_PATH/checks/functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-checks-report() { - declare desc="shows reports for an app" - declare cmd="checks:report" - [[ "$1" == "$cmd" ]] && shift 1 - fn-report-parse-args "$@" - set -- "${REPORT_ARGS[@]}" - declare APP="${1:-}" INFO_FLAG="${2:-}" - - if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then - INFO_FLAG="$APP" - APP="" - fi - - if [[ "$REPORT_IS_GLOBAL" == "true" ]]; then - APP="--global" - fi - - if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then - INFO_FLAG="true" - fi - - if [[ "$APP" == "--global" ]]; then - cmd-checks-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - elif [[ -z "$APP" ]]; then - for app in $(dokku_apps); do - cmd-checks-report-single "$app" "$INFO_FLAG" "$REPORT_FORMAT" | tee || true - done - else - cmd-checks-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - fi -} - -cmd-checks-report-single() { - declare APP="$1" INFO_FLAG="$2" FORMAT="${3:-stdout}" - if [[ "$INFO_FLAG" == "true" ]]; then - INFO_FLAG="" - fi - local flag_map=() - if [[ "$APP" == "--global" ]]; then - flag_map=( - "--checks-computed-wait-to-retire: $(fn-checks-computed-wait-to-retire "$APP")" - "--checks-global-wait-to-retire: $(fn-checks-global-wait-to-retire "$APP")" - ) - else - verify_app_name "$APP" - flag_map=( - "--checks-disabled-list: $(fn-checks-disabled-list "$APP")" - "--checks-skipped-list: $(fn-checks-skipped-list "$APP")" - "--checks-computed-wait-to-retire: $(fn-checks-computed-wait-to-retire "$APP")" - "--checks-global-wait-to-retire: $(fn-checks-global-wait-to-retire "$APP")" - "--checks-wait-to-retire: $(fn-checks-wait-to-retire "$APP")" - ) - fi - - fn-report-validate-format "$FORMAT" "$INFO_FLAG" - - if [[ "$FORMAT" == "json" ]]; then - fn-report-emit-json flag_map "checks" - return - fi - - if [[ -z "$INFO_FLAG" ]]; then - if [[ "$APP" == "--global" ]]; then - dokku_log_info2_quiet "global checks information" - else - dokku_log_info2_quiet "$APP checks information" - fi - 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 - 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 - else - match=true - fi - fi - done - [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" - fi -} - fn-checks-disabled-list() { declare APP="$1" diff --git a/plugins/checks/report b/plugins/checks/report deleted file mode 100755 index 558804912..000000000 --- a/plugins/checks/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/checks/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-checks-report-single "$@" diff --git a/plugins/checks/report.go b/plugins/checks/report.go new file mode 100644 index 000000000..e93f48a78 --- /dev/null +++ b/plugins/checks/report.go @@ -0,0 +1,86 @@ +package checks + +import ( + "github.com/dokku/dokku/plugins/common" +) + +// ReportSingleApp is an internal function that displays the checks report for one or more apps +func ReportSingleApp(appName string, format string, infoFlag string) error { + if appName != "--global" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + } + + var flags map[string]common.ReportFunc + if appName == "--global" { + flags = map[string]common.ReportFunc{ + "--checks-computed-wait-to-retire": reportComputedWaitToRetire, + "--checks-global-wait-to-retire": reportGlobalWaitToRetire, + } + } else { + flags = map[string]common.ReportFunc{ + "--checks-disabled-list": reportDisabledList, + "--checks-skipped-list": reportSkippedList, + "--checks-computed-wait-to-retire": reportComputedWaitToRetire, + "--checks-global-wait-to-retire": reportGlobalWaitToRetire, + "--checks-wait-to-retire": reportWaitToRetire, + } + } + + flagKeys := []string{} + for flagKey := range flags { + flagKeys = append(flagKeys, flagKey) + } + + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp(common.ReportSingleAppInput{ + ReportType: "checks", + AppName: appName, + InfoFlag: infoFlag, + InfoFlags: infoFlags, + InfoFlagKeys: flagKeys, + Format: format, + TrimPrefix: true, + UppercaseFirstCharacter: true, + EmitLegacyPrefix: false, + }) +} + +func reportDisabledList(appName string) string { + value := common.PropertyGet("checks", appName, "disabled") + if value == "" { + value = "none" + } + + return value +} + +func reportSkippedList(appName string) string { + value := common.PropertyGet("checks", appName, "skipped") + if value == "" { + value = "none" + } + + return value +} + +func reportWaitToRetire(appName string) string { + return common.PropertyGet("checks", appName, "wait-to-retire") +} + +func reportGlobalWaitToRetire(appName string) string { + return common.PropertyGet("checks", "--global", "wait-to-retire") +} + +func reportComputedWaitToRetire(appName string) string { + value := reportWaitToRetire(appName) + if value == "" { + value = reportGlobalWaitToRetire(appName) + } + if value == "" { + value = "60" + } + + return value +} diff --git a/plugins/checks/src/subcommands/subcommands.go b/plugins/checks/src/subcommands/subcommands.go new file mode 100644 index 000000000..fdecaa599 --- /dev/null +++ b/plugins/checks/src/subcommands/subcommands.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "strings" + + checks "github.com/dokku/dokku/plugins/checks" + "github.com/dokku/dokku/plugins/common" + + flag "github.com/spf13/pflag" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + case "report": + args := flag.NewFlagSet("checks:report", flag.ExitOnError) + format := args.String("format", "stdout", "format: [ stdout | json ]") + reportArgs, flagErr := common.ParseReportArgs("checks", os.Args[2:]) + if flagErr == nil { + args.Parse(reportArgs.OSArgs) + appName := args.Arg(0) + if reportArgs.IsGlobal { + appName = "--global" + } + err = checks.CommandReport(appName, *format, reportArgs.InfoFlag) + } + default: + err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/checks/src/triggers/triggers.go b/plugins/checks/src/triggers/triggers.go new file mode 100644 index 000000000..29732db72 --- /dev/null +++ b/plugins/checks/src/triggers/triggers.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + checks "github.com/dokku/dokku/plugins/checks" + "github.com/dokku/dokku/plugins/common" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "report": + appName := flag.Arg(0) + err = checks.ReportSingleApp(appName, "", "") + default: + err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/checks/subcommands.go b/plugins/checks/subcommands.go new file mode 100644 index 000000000..22554a536 --- /dev/null +++ b/plugins/checks/subcommands.go @@ -0,0 +1,29 @@ +package checks + +import ( + "errors" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandReport displays a checks report for one or more apps +func CommandReport(appName string, format string, infoFlag string) error { + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + if errors.Is(err, common.NoAppsExist) { + common.LogWarn(err.Error()) + return nil + } + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, format, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, format, infoFlag) +} diff --git a/plugins/checks/subcommands/report b/plugins/checks/subcommands/report deleted file mode 100755 index b0006b8bb..000000000 --- a/plugins/checks/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/checks/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-checks-report "$@" diff --git a/plugins/domains/.gitignore b/plugins/domains/.gitignore new file mode 100644 index 000000000..dac9d7731 --- /dev/null +++ b/plugins/domains/.gitignore @@ -0,0 +1,4 @@ +/report +/report-subcommand +/triggers +/subcommands/report diff --git a/plugins/domains/Makefile b/plugins/domains/Makefile new file mode 100644 index 000000000..c4698f30c --- /dev/null +++ b/plugins/domains/Makefile @@ -0,0 +1,13 @@ +GOARCH ?= amd64 +TRIGGERS = triggers/report +BUILD = report-subcommand triggers +PLUGIN_NAME = domains + +clean-report-subcommand: + rm -rf report-subcommand + +report-subcommand: clean-report-subcommand src/subcommands/subcommands.go + GOARCH=$(GOARCH) go build -mod=readonly -ldflags="-s -w" $(GO_ARGS) -o report-subcommand src/subcommands/subcommands.go + ln -sf ../report-subcommand subcommands/report + +include ../../common.mk diff --git a/plugins/domains/go.mod b/plugins/domains/go.mod new file mode 100644 index 000000000..42fd7dd0f --- /dev/null +++ b/plugins/domains/go.mod @@ -0,0 +1,29 @@ +module github.com/dokku/dokku/plugins/domains + +go 1.26.2 + +require ( + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/spf13/pflag v1.0.10 +) + +require ( + github.com/alexellis/go-execute/v2 v2.2.1 // indirect + github.com/fatih/color v1.19.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.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/melbahja/goph v1.5.1 // 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.10 // indirect + github.com/ryanuber/columnize v2.1.2+incompatible // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect +) + +replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/domains/go.sum b/plugins/domains/go.sum new file mode 100644 index 000000000..5a5292407 --- /dev/null +++ b/plugins/domains/go.sum @@ -0,0 +1,55 @@ +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +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/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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +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.5.1 h1:gdZyjn5nM6FO6mAZrv7XqE2gArQbb+V6FdIyH3Ru3vA= +github.com/melbahja/goph v1.5.1/go.mod h1:dDwo+44cmvfDLdiVpc6fJxexf5BA5yEDUeE5YgtuDO4= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +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.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +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/domains/internal-functions b/plugins/domains/internal-functions old mode 100755 new mode 100644 index 7699d5888..04f68bef6 --- a/plugins/domains/internal-functions +++ b/plugins/domains/internal-functions @@ -4,95 +4,6 @@ source "$PLUGIN_AVAILABLE_PATH/domains/functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -cmd-domains-report() { - declare desc="displays a domains report for one or more apps" - declare cmd="domains:report" - [[ "$1" == "$cmd" ]] && shift 1 - fn-report-parse-args "$@" - set -- "${REPORT_ARGS[@]}" - declare APP="${1:-}" INFO_FLAG="${2:-}" - - if [[ -n "$APP" ]] && [[ "$APP" != "--global" ]] && [[ "$APP" == --* ]]; then - INFO_FLAG="$APP" - APP="" - fi - - if [[ "$REPORT_IS_GLOBAL" == "true" ]]; then - APP="--global" - fi - - if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then - INFO_FLAG="true" - fi - - if [[ "$APP" == "--global" ]]; then - cmd-domains-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - elif [[ -z "$APP" ]]; then - for app in $(dokku_apps); do - cmd-domains-report-single "$app" "$INFO_FLAG" "$REPORT_FORMAT" | tee || true - done - else - cmd-domains-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - fi -} - -cmd-domains-report-single() { - declare APP="$1" INFO_FLAG="$2" FORMAT="${3:-stdout}" - if [[ "$INFO_FLAG" == "true" ]]; then - INFO_FLAG="" - fi - local flag_map=() app_flags=() global_flags=() - - if [[ "$APP" != "--global" ]]; then - verify_app_name "$APP" - app_flags=( - "--domains-app-enabled: $(fn-domains-app-enabled "$APP")" - "--domains-app-vhosts: $(fn-domains-app-vhosts "$APP" | awk '{$1=$1};1')" - ) - fi - - global_flags=( - "--domains-global-enabled: $(fn-domains-global-enabled)" - "--domains-global-vhosts: $(fn-domains-global-vhosts | awk '{$1=$1};1')" - ) - - flag_map=("${app_flags[@]}" "${global_flags[@]}") - - fn-report-validate-format "$FORMAT" "$INFO_FLAG" - - if [[ "$FORMAT" == "json" ]]; then - fn-report-emit-json flag_map "domains" - return - fi - - if [[ -z "$INFO_FLAG" ]]; then - if [[ "$APP" == "--global" ]]; then - dokku_log_info2_quiet "Global domains information" - else - dokku_log_info2_quiet "$APP domains information" - fi - 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 - 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 - else - match=true - fi - fi - done - [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" - fi -} - fn-domains-app-enabled() { declare APP="$1" local DOMAINS_APP_ENABLED=false diff --git a/plugins/domains/report b/plugins/domains/report deleted file mode 100755 index dde95bcd7..000000000 --- a/plugins/domains/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/domains/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-domains-report-single "$@" diff --git a/plugins/domains/report.go b/plugins/domains/report.go new file mode 100644 index 000000000..cf5788393 --- /dev/null +++ b/plugins/domains/report.go @@ -0,0 +1,124 @@ +package domains + +import ( + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +var hostnameRegex = regexp.MustCompile(`^[a-z0-9.*-]*[a-z0-9*-]$`) + +// ReportSingleApp is an internal function that displays the domains report for one or more apps +func ReportSingleApp(appName string, format string, infoFlag string) error { + if appName != "--global" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + } + + var flags map[string]common.ReportFunc + if appName == "--global" { + flags = map[string]common.ReportFunc{ + "--domains-global-enabled": reportGlobalEnabled, + "--domains-global-vhosts": reportGlobalVhosts, + } + } else { + flags = map[string]common.ReportFunc{ + "--domains-app-enabled": reportAppEnabled, + "--domains-app-vhosts": reportAppVhosts, + "--domains-global-enabled": reportGlobalEnabled, + "--domains-global-vhosts": reportGlobalVhosts, + } + } + + flagKeys := []string{} + for flagKey := range flags { + flagKeys = append(flagKeys, flagKey) + } + + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp(common.ReportSingleAppInput{ + ReportType: "domains", + AppName: appName, + InfoFlag: infoFlag, + InfoFlags: infoFlags, + InfoFlagKeys: flagKeys, + Format: format, + TrimPrefix: true, + UppercaseFirstCharacter: true, + EmitLegacyPrefix: false, + }) +} + +func reportAppEnabled(appName string) string { + results, _ := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "config-get", + Args: []string{appName, "NO_VHOST"}, + }) + if results.StdoutContents() == "1" { + return "false" + } + + return "true" +} + +func reportAppVhosts(appName string) string { + dokkuRoot := common.MustGetEnv("DOKKU_ROOT") + return normalizeWhitespace(readFileString(filepath.Join(dokkuRoot, appName, "VHOST"))) +} + +func reportGlobalEnabled(appName string) string { + if isGlobalVhostEnabled() { + return "true" + } + + return "false" +} + +func reportGlobalVhosts(appName string) string { + if !isGlobalVhostEnabled() { + return "" + } + + return normalizeWhitespace(globalVhostContents()) +} + +func globalVhostContents() string { + dokkuRoot := common.MustGetEnv("DOKKU_ROOT") + return readFileString(filepath.Join(dokkuRoot, "VHOST")) +} + +func isGlobalVhostEnabled() bool { + for _, line := range strings.Split(globalVhostContents(), "\n") { + if isValidHostname(line) { + return true + } + } + + return false +} + +func isValidHostname(hostname string) bool { + hostname = strings.ToLower(hostname) + if hostname == "_" { + return true + } + + return hostnameRegex.MatchString(hostname) +} + +func readFileString(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + + return string(data) +} + +func normalizeWhitespace(value string) string { + return strings.Join(strings.Fields(value), " ") +} diff --git a/plugins/domains/src/subcommands/subcommands.go b/plugins/domains/src/subcommands/subcommands.go new file mode 100644 index 000000000..71eb086d5 --- /dev/null +++ b/plugins/domains/src/subcommands/subcommands.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + domains "github.com/dokku/dokku/plugins/domains" + + flag "github.com/spf13/pflag" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + case "report": + args := flag.NewFlagSet("domains:report", flag.ExitOnError) + format := args.String("format", "stdout", "format: [ stdout | json ]") + reportArgs, flagErr := common.ParseReportArgs("domains", os.Args[2:]) + if flagErr == nil { + args.Parse(reportArgs.OSArgs) + appName := args.Arg(0) + if reportArgs.IsGlobal { + appName = "--global" + } + err = domains.CommandReport(appName, *format, reportArgs.InfoFlag) + } + default: + err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/domains/src/triggers/triggers.go b/plugins/domains/src/triggers/triggers.go new file mode 100644 index 000000000..62889aa28 --- /dev/null +++ b/plugins/domains/src/triggers/triggers.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + domains "github.com/dokku/dokku/plugins/domains" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "report": + appName := flag.Arg(0) + err = domains.ReportSingleApp(appName, "", "") + default: + err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/domains/subcommands.go b/plugins/domains/subcommands.go new file mode 100644 index 000000000..e20ac6352 --- /dev/null +++ b/plugins/domains/subcommands.go @@ -0,0 +1,29 @@ +package domains + +import ( + "errors" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandReport displays a domains report for one or more apps +func CommandReport(appName string, format string, infoFlag string) error { + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + if errors.Is(err, common.NoAppsExist) { + common.LogWarn(err.Error()) + return nil + } + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, format, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, format, infoFlag) +} diff --git a/plugins/domains/subcommands/report b/plugins/domains/subcommands/report deleted file mode 100755 index cb6efbb7d..000000000 --- a/plugins/domains/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/domains/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-domains-report "$@" diff --git a/tests/unit/domains.bats b/tests/unit/domains.bats index f46882c2d..005973044 100644 --- a/tests/unit/domains.bats +++ b/tests/unit/domains.bats @@ -44,13 +44,19 @@ teardown() { echo "output: $output" echo "status: $status" assert_success - assert_output_contains "Global domains information" + assert_output_contains "global domains information" run /bin/bash -c "dokku domains:report --global --domains-global-enabled" echo "output: $output" echo "status: $status" assert_success assert_output "true" + + run /bin/bash -c "dokku domains:report --global --domains-invalid-flag" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "Invalid flag passed" } @test "(domains) domains" {