diff --git a/go.work b/go.work index afe21daad..2aa88dabc 100644 --- a/go.work +++ b/go.work @@ -12,6 +12,7 @@ use ( ./plugins/builder-railpack ./plugins/buildpacks ./plugins/caddy-vhosts + ./plugins/certs ./plugins/checks ./plugins/common ./plugins/config diff --git a/plugins/certs/.gitignore b/plugins/certs/.gitignore new file mode 100644 index 000000000..dac9d7731 --- /dev/null +++ b/plugins/certs/.gitignore @@ -0,0 +1,4 @@ +/report +/report-subcommand +/triggers +/subcommands/report diff --git a/plugins/certs/Makefile b/plugins/certs/Makefile new file mode 100644 index 000000000..92ef9be66 --- /dev/null +++ b/plugins/certs/Makefile @@ -0,0 +1,13 @@ +GOARCH ?= amd64 +TRIGGERS = triggers/report +BUILD = report-subcommand triggers +PLUGIN_NAME = certs + +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/certs/go.mod b/plugins/certs/go.mod new file mode 100644 index 000000000..28a0fda20 --- /dev/null +++ b/plugins/certs/go.mod @@ -0,0 +1,29 @@ +module github.com/dokku/dokku/plugins/certs + +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/certs/go.sum b/plugins/certs/go.sum new file mode 100644 index 000000000..5a5292407 --- /dev/null +++ b/plugins/certs/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/certs/internal-functions b/plugins/certs/internal-functions old mode 100755 new mode 100644 index 2686c0a0c..be7f27578 --- a/plugins/certs/internal-functions +++ b/plugins/certs/internal-functions @@ -46,165 +46,3 @@ fn-certs-remove() { plugn trigger post-domains-update "$APP" } -cmd-certs-report() { - declare desc="displays an ssl report for one or more apps" - declare cmd="certs: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-certs-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - elif [[ -z "$APP" ]]; then - for app in $(dokku_apps); do - cmd-certs-report-single "$app" "$INFO_FLAG" "$REPORT_FORMAT" | tee || true - done - else - cmd-certs-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - fi -} - -cmd-certs-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 - verify_app_name "$APP" - flag_map=( - "--ssl-dir: $DOKKU_ROOT/$APP/tls" - "--ssl-enabled: $(fn-ssl-enabled "$APP")" - "--ssl-hostnames: $(fn-ssl-hostnames "$APP")" - "--ssl-expires-at: $(fn-ssl-expires-at "$APP")" - "--ssl-issuer: $(fn-ssl-issuer "$APP")" - "--ssl-starts-at: $(fn-ssl-starts-at "$APP")" - "--ssl-subject: $(fn-ssl-subject "$APP")" - "--ssl-verified: $(fn-ssl-verified "$APP")" - ) - fi - - fn-report-validate-format "$FORMAT" "$INFO_FLAG" - - if [[ "$FORMAT" == "json" ]]; then - fn-report-emit-json flag_map "ssl" - return - fi - - if [[ -z "$INFO_FLAG" ]]; then - if [[ "$APP" == "--global" ]]; then - dokku_log_info2_quiet "global ssl information" - else - dokku_log_info2_quiet "$APP ssl 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-ssl-enabled() { - declare APP="$1" - local SSL_ENABLED=false - - if is_ssl_enabled "$APP"; then - SSL_ENABLED=true - fi - echo "$SSL_ENABLED" -} - -fn-ssl-expires-at() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Not After :" | awk -F " : " '{ print $2 }' - fi -} - -fn-ssl-hostnames() { - declare APP="$1" - - if is_ssl_enabled "$APP"; then - get_ssl_hostnames "$APP" | xargs - fi -} - -fn-ssl-issuer() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Issuer:" | head -n1 | sed -e 's/Issuer: //g' -e 's/^[[:space:]]*//' - fi -} - -fn-ssl-starts-at() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Not Before:" | awk -F ": " '{ print $2 }' - fi -} - -fn-ssl-subject() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -subject | sed -e "s:subject= ::g" | sed -e "s:^/::g" | sed -e "s:/:; :g" - fi -} - -fn-ssl-verified() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - local SSL_VERIFY_OUTPUT=false SSL_SELF_SIGNED="self signed" - - if ! is_ssl_enabled "$APP"; then - return - fi - - if [[ -f "$APP_SSL_PATH/server.letsencrypt.crt" ]]; then - SSL_VERIFY_OUTPUT="$(openssl verify -verbose -purpose sslserver -CAfile "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.letsencrypt.crt" 2>/dev/null | awk -F ':' '{ print $2 }' | tail -1 | xargs || true)" - else - SSL_VERIFY_OUTPUT="$(openssl verify -verbose -purpose sslserver "$APP_SSL_PATH/server.crt" 2>/dev/null | awk -F ':' '{ print $2 }' | tail -1 | xargs || true)" - fi - - if [[ "$SSL_VERIFY_OUTPUT" == "OK" ]]; then - SSL_SELF_SIGNED="verified by a certificate authority" - fi - - echo "$SSL_SELF_SIGNED" -} diff --git a/plugins/certs/report b/plugins/certs/report deleted file mode 100755 index b323feb51..000000000 --- a/plugins/certs/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-certs-report-single "$@" diff --git a/plugins/certs/report.go b/plugins/certs/report.go new file mode 100644 index 000000000..02ee5ec15 --- /dev/null +++ b/plugins/certs/report.go @@ -0,0 +1,233 @@ +package certs + +import ( + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +var dnsPrefixRegex = regexp.MustCompile(`[[:space:]]*DNS:`) + +// ReportSingleApp is an internal function that displays the ssl report for one or more apps +func ReportSingleApp(appName string, format string, infoFlag string) error { + flags := map[string]common.ReportFunc{} + if appName != "--global" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + flags = map[string]common.ReportFunc{ + "--ssl-dir": reportSSLDir, + "--ssl-enabled": reportSSLEnabled, + "--ssl-hostnames": reportSSLHostnames, + "--ssl-expires-at": reportSSLExpiresAt, + "--ssl-issuer": reportSSLIssuer, + "--ssl-starts-at": reportSSLStartsAt, + "--ssl-subject": reportSSLSubject, + "--ssl-verified": reportSSLVerified, + } + } + + flagKeys := []string{} + for flagKey := range flags { + flagKeys = append(flagKeys, flagKey) + } + + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp(common.ReportSingleAppInput{ + ReportType: "ssl", + AppName: appName, + InfoFlag: infoFlag, + InfoFlags: infoFlags, + InfoFlagKeys: flagKeys, + Format: format, + TrimPrefix: true, + UppercaseFirstCharacter: true, + EmitLegacyPrefix: false, + }) +} + +func certTLSPath(appName string) string { + return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "tls") +} + +func isSSLEnabled(appName string) bool { + tlsPath := certTLSPath(appName) + if _, err := os.Stat(filepath.Join(tlsPath, "server.crt")); err != nil { + return false + } + if _, err := os.Stat(filepath.Join(tlsPath, "server.key")); err != nil { + return false + } + + return true +} + +func opensslCertText(appName string) string { + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-text"}, + }) + if err != nil { + return "" + } + + return result.Stdout +} + +func reportSSLDir(appName string) string { + return certTLSPath(appName) +} + +func reportSSLEnabled(appName string) string { + if isSSLEnabled(appName) { + return "true" + } + + return "false" +} + +func reportSSLExpiresAt(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Not After :") { + parts := strings.Split(line, " : ") + if len(parts) >= 2 { + return parts[1] + } + } + } + + return "" +} + +func reportSSLStartsAt(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Not Before:") { + parts := strings.Split(line, ": ") + if len(parts) >= 2 { + return parts[1] + } + } + } + + return "" +} + +func reportSSLIssuer(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Issuer:") { + line = strings.Replace(line, "Issuer: ", "", 1) + return strings.TrimLeft(line, " \t") + } + } + + return "" +} + +func reportSSLSubject(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject"}, + }) + if err != nil { + return "" + } + + subject := strings.Replace(result.StdoutContents(), "subject= ", "", 1) + subject = strings.TrimPrefix(subject, "/") + return strings.ReplaceAll(subject, "/", "; ") +} + +func reportSSLVerified(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + tlsPath := certTLSPath(appName) + args := []string{"verify", "-verbose", "-purpose", "sslserver"} + if _, err := os.Stat(filepath.Join(tlsPath, "server.letsencrypt.crt")); err == nil { + args = append(args, "-CAfile", filepath.Join(tlsPath, "server.crt"), filepath.Join(tlsPath, "server.letsencrypt.crt")) + } else { + args = append(args, filepath.Join(tlsPath, "server.crt")) + } + + result, _ := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: args, + }) + + verifyOutput := "" + if out := strings.TrimRight(result.Stdout, "\n"); out != "" { + lines := strings.Split(out, "\n") + parts := strings.Split(lines[len(lines)-1], ":") + if len(parts) >= 2 { + verifyOutput = strings.TrimSpace(parts[1]) + } + } + + if verifyOutput == "OK" { + return "verified by a certificate authority" + } + + return "self signed" +} + +func reportSSLHostnames(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + hostnameSet := map[string]bool{} + + subjectResult, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject"}, + }) + if err == nil { + for _, part := range strings.Split(subjectResult.StdoutContents(), "/") { + if strings.Contains(part, "CN=") && len(part) > 3 { + hostnameSet[part[3:]] = true + } + } + } + + textLines := strings.Split(opensslCertText(appName), "\n") + for i, line := range textLines { + if strings.Contains(line, "509v3 Subject Alternative Name:") && i+1 < len(textLines) { + sanLine := dnsPrefixRegex.ReplaceAllString(textLines[i+1], "") + for _, name := range strings.Split(sanLine, ",") { + name = strings.TrimSpace(name) + if name != "" { + hostnameSet[name] = true + } + } + } + } + + hostnames := []string{} + for name := range hostnameSet { + hostnames = append(hostnames, name) + } + sort.Strings(hostnames) + + return strings.Join(hostnames, " ") +} diff --git a/plugins/certs/src/subcommands/subcommands.go b/plugins/certs/src/subcommands/subcommands.go new file mode 100644 index 000000000..4027b90b8 --- /dev/null +++ b/plugins/certs/src/subcommands/subcommands.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "strings" + + certs "github.com/dokku/dokku/plugins/certs" + "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("certs:report", flag.ExitOnError) + format := args.String("format", "stdout", "format: [ stdout | json ]") + reportArgs, flagErr := common.ParseReportArgs("certs", os.Args[2:]) + if flagErr == nil { + args.Parse(reportArgs.OSArgs) + appName := args.Arg(0) + if reportArgs.IsGlobal { + appName = "--global" + } + err = certs.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/certs/src/triggers/triggers.go b/plugins/certs/src/triggers/triggers.go new file mode 100644 index 000000000..245fa8038 --- /dev/null +++ b/plugins/certs/src/triggers/triggers.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + certs "github.com/dokku/dokku/plugins/certs" + "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 = certs.ReportSingleApp(appName, "", "") + default: + err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/certs/subcommands.go b/plugins/certs/subcommands.go new file mode 100644 index 000000000..56521baf3 --- /dev/null +++ b/plugins/certs/subcommands.go @@ -0,0 +1,29 @@ +package certs + +import ( + "errors" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandReport displays a certs 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/certs/subcommands/report b/plugins/certs/subcommands/report deleted file mode 100755 index 054bbf7ca..000000000 --- a/plugins/certs/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-certs-report "$@"