Commit f7fff1cb authored by dominik.rimpf's avatar dominik.rimpf 🐿 Committed by peter.oettig
Browse files

Moved to better cli argument parsing with viper.

This allows to pass all cli options as environment variables and fixes an issue were challenges starting with a dash were incorrectly interpreted as cli option instead of a positional argument.
parent 44746828
Pipeline #240884 passed with stages
in 1 minute and 58 seconds
# NETVS config
_test_token_*
netdb_client.ini*
.testing
# builds
/build
......
......@@ -39,6 +39,7 @@ test:
# this wraps around go test command & generates test report for gitlab
# test with race-detector, coverprofile output and covermode (how many times did each statement run?)
- gotestsum --junitfile report.xml --format standard-verbose -- -race -coverprofile=.testCoverage.txt -covermode atomic
after_script:
# for coverage report in gitlab
- gocover-cobertura < .testCoverage.txt > coverage.xml
coverage: '/coverage: \d+.\d+% of statements/'
......
0.3.6
0.4.0
\ No newline at end of file
......@@ -21,7 +21,8 @@ RUN --mount=type=secret,id=REPO_USER \
golang-github-miekg-dns-dev \
golang-github-mitchellh-go-homedir-dev \
golang-github-mitchellh-mapstructure-dev \
golang-github-spf13-pflag-dev \
golang-github-spf13-cobra-dev \
golang-github-spf13-viper-dev \
golang-github-stretchr-testify-dev \
golang-gopkg-h2non-gock.v1-dev \
golang-gopkg-ini.v1-dev \
......
package cmd
import (
"fmt"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"log"
"os"
)
const ConfigName = "acme4netvs_certbot_auth_hook"
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
var (
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
rootCmd = &cobra.Command{
Use: "acme4netvs_certbot_auth_hook",
Short: "ACME DNS-01 challenge hook for certbot and KIT NETVS",
Long: `
acme4netvs_certbot_auth_hook creates the requested ACME DNS-01 TXT record for the given domain.
This is usually called directly by certbot.
acme4netvs_certbot_auth_hook requires two mandatory environment variables:
CERTBOT_DOMAIN: The domain being authenticated
CERTBOT_VALIDATION: The validation string
Options that may be set using environment variables lists the matching variable names in square brackets.
`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
apiClient = acme4netvs.BuildLocalNetVSClient(DebugPrintf)
viper.MustBindEnv("certbotdomain", "CERTBOT_DOMAIN")
if !viper.IsSet("certbotdomain") || len(viper.GetString("certbotdomain")) == 0 {
log.Fatal(acme4netvs.FormatEmptyDomain)
}
fqdn := viper.GetString("certbotdomain")
viper.MustBindEnv("certbotvalidation", "CERTBOT_VALIDATION")
if !viper.IsSet("certbotvalidation") || len(viper.GetString("certbotvalidation")) == 0 {
log.Fatalf(acme4netvs.FormatEmptyValidation, fqdn)
}
token := viper.GetString("certbotvalidation")
err := acme4netvs.DeployChallenge(apiClient, fqdn, token, DebugPrintf)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeDeployment, fqdn, err)
}
if viper.GetBool("dns-wait") {
acme4netvs.CheckChallengeOnAllNS(
token,
fqdn,
viper.GetDuration("dns-wait-timeout"),
viper.GetDuration("dns-wait-between"),
DebugPrintf,
)
}
},
Version: fmt.Sprintf("Version %s build on %s (commit %s)\n", version, date, commit),
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
acme4netvs.CommonViperSetup(rootCmd)
acme4netvs.CommonGlobalFlags(ConfigName, rootCmd)
acme4netvs.CommonDNSWaitFlags(rootCmd)
DebugPrintf = acme4netvs.FinalizeViperSetup(rootCmd)
}
//go:generate goversioninfo -icon=../../../res/acme4netvs-icon.ico -arm=true
package main
import (
"fmt"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
flag "github.com/spf13/pflag"
"log"
"os"
"path"
)
var (
config acme4netvs.ClientConfig
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
)
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
func init() {
// custom help message
flag.Usage = func() {
fmt.Fprint(os.Stderr, "Usage (this is usually called directly by certbot):\n\n")
fmt.Fprintf(os.Stderr, " %s [OPTIONS]\n\n", path.Base(os.Args[0]))
fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
flag.PrintDefaults()
fmt.Fprint(os.Stderr, `
Mandatory environment variables:
CERTBOT_DOMAIN: The domain being authenticated
CERTBOT_VALIDATION: The validation string
Optional environment variables:
CERTBOT_TOKEN: Resource name part of the HTTP-01 challenge
CERTBOT_REMAINING_CHALLENGES: Number of challenges remaining after the current challenge
CERTBOT_ALL_DOMAINS: A comma-separated list of all domains challenged for the current certificate
`)
fmt.Fprintf(os.Stderr, "Version %s build on %s (commit %s)\n", version, date, commit)
fmt.Fprintln(os.Stderr, acme4netvs.ReleaseURLText)
os.Exit(0)
}
// parse flags
acme4netvs.DefineCommonFlags(&config)
acme4netvs.DefineCommonWaitingFlags(&config)
flag.Parse()
DebugPrintf = acme4netvs.MakeDebugFunc(!config.Quiet, os.Stdout)
log.SetFlags(log.Lshortfile | log.Ltime)
// build NETVS client
apiClient = acme4netvs.CreateNETVSClient(&config, DebugPrintf)
DebugPrintf("🚀 NETVS client created\n")
}
import "git.scc.kit.edu/KIT-CA/acme4netvs/v2/cmd/certbot_netvs_hooks/certbot_netvs_auth_hook/cmd"
func main() {
certbotArgs := acme4netvs.GetCertbotArguments()
DebugPrintf("✨ Parsed arguments: %+#v\n", certbotArgs)
if certbotArgs.Domain == "" {
log.Fatal(acme4netvs.FormatEmptyDomain)
}
if certbotArgs.Validation == "" {
log.Fatalf(acme4netvs.FormatEmptyValidation, certbotArgs.Domain)
}
err := acme4netvs.DeployChallenge(apiClient, certbotArgs.Domain, certbotArgs.Validation)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeDeployment, certbotArgs.Domain, err)
}
if config.WaitForDNS {
acme4netvs.CheckChallengeOnAllNS(
certbotArgs.Validation,
certbotArgs.Domain,
config.DNSWaitTimeout,
config.DNSTimeBetweenChecks,
DebugPrintf,
)
}
cmd.Execute()
}
package cmd
import (
"fmt"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"log"
"os"
)
const ConfigName = "acme4netvs_certbot_cleanup_hook"
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
var (
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
rootCmd = &cobra.Command{
Use: "acme4netvs_certbot_cleanup_hook",
Short: "ACME DNS-01 cleanup hook for certbot and KIT NETVS",
Long: `
acme4netvs_certbot_cleanup_hook removes the TXT record that was created by deploy_challenge.
This is usually called directly by certbot.
acme4netvs_certbot_cleanup_hook requires two mandatory environment variables:
CERTBOT_DOMAIN: The domain being authenticated
CERTBOT_VALIDATION: The validation string
Options that may be set using environment variables lists the matching variable names in square brackets.
`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
apiClient = acme4netvs.BuildLocalNetVSClient(DebugPrintf)
viper.MustBindEnv("certbotdomain", "CERTBOT_DOMAIN")
if !viper.IsSet("certbotdomain") || len(viper.GetString("certbotdomain")) == 0 {
log.Fatal(acme4netvs.FormatEmptyDomain)
}
fqdn := viper.GetString("certbotdomain")
viper.MustBindEnv("certbotvalidation", "CERTBOT_VALIDATION")
if !viper.IsSet("certbotvalidation") || len(viper.GetString("certbotvalidation")) == 0 {
log.Fatalf(acme4netvs.FormatEmptyValidation, fqdn)
}
token := viper.GetString("certbotvalidation")
err := acme4netvs.CleanChallenge(apiClient, fqdn, token, DebugPrintf)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeCleanup, fqdn, err)
}
},
Version: fmt.Sprintf("Version %s build on %s (commit %s)\n", version, date, commit),
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
acme4netvs.CommonViperSetup(rootCmd)
acme4netvs.CommonGlobalFlags(ConfigName, rootCmd)
acme4netvs.CommonDNSWaitFlags(rootCmd)
DebugPrintf = acme4netvs.FinalizeViperSetup(rootCmd)
}
//go:generate goversioninfo -icon=../../../res/acme4netvs-icon.ico -arm=true
package main
import (
"fmt"
"log"
"os"
"path"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
flag "github.com/spf13/pflag"
)
var (
config acme4netvs.ClientConfig
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
)
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
func init() {
// custom help message
flag.Usage = func() {
fmt.Fprint(os.Stderr, "Usage (this is usually called directly by certbot):\n\n")
fmt.Fprintf(os.Stderr, " %s [OPTIONS]\n\n", path.Base(os.Args[0]))
fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
flag.PrintDefaults()
fmt.Fprint(os.Stderr, `
Mandatory environment variables:
CERTBOT_DOMAIN: The domain being authenticated
CERTBOT_VALIDATION: The validation string
Optional environment variables:
CERTBOT_TOKEN: Resource name part of the HTTP-01 challenge
CERTBOT_REMAINING_CHALLENGES: Number of challenges remaining after the current challenge
CERTBOT_ALL_DOMAINS: A comma-separated list of all domains challenged for the current certificate
CERTBOT_AUTH_OUTPUT: Whatever the auth script wrote to stdout
`)
fmt.Fprintf(os.Stderr, "Version %s build on %s (commit %s)\n", version, date, commit)
fmt.Fprintln(os.Stderr, acme4netvs.ReleaseURLText)
os.Exit(0)
}
// parse flags
acme4netvs.DefineCommonFlags(&config)
flag.Parse()
// return no error of only --help is provided
flag.ErrHelp = nil
DebugPrintf = acme4netvs.MakeDebugFunc(!config.Quiet, os.Stdout)
log.SetFlags(log.Llongfile)
// build NETVS client
apiClient = acme4netvs.CreateNETVSClient(&config, DebugPrintf)
DebugPrintf("🚀 NETVS client created\n")
}
import "git.scc.kit.edu/KIT-CA/acme4netvs/v2/cmd/certbot_netvs_hooks/certbot_netvs_cleanup_hook/cmd"
func main() {
certbotArgs := acme4netvs.GetCertbotArguments()
DebugPrintf("✨ Parsed arguments: %+#v\n", certbotArgs)
if certbotArgs.Domain == "" {
log.Fatal(acme4netvs.FormatEmptyDomain)
}
if certbotArgs.Validation == "" {
log.Fatalf(acme4netvs.FormatEmptyValidation, certbotArgs.Domain)
}
err := acme4netvs.CleanChallenge(apiClient, certbotArgs.Domain, certbotArgs.Validation)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeCleanup, certbotArgs.Domain, err)
}
cmd.Execute()
}
package cmd
import (
"fmt"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"log"
"os"
)
const ConfigName = "dehydrated_netvs_hook"
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
var (
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
rootCmd = &cobra.Command{
Use: "acme4netvs_dehydrated",
Short: "ACME DNS-01 hook for dehydrated and KIT NETVS",
Long: `
ACME DNS-01 hook for dehydrated and KIT NETVS.
This is usually called directly by dehydrated.
Options that may be set using environment variables lists the matching variable names in square brackets.
`,
Version: fmt.Sprintf("Version %s build on %s (commit %s)\n", version, date, commit),
Run: func(cmd *cobra.Command, args []string) {
// do nothing, this just accepts unknown arguments
if len(args) > 0 {
DebugPrintf(acme4netvs.FormatCommandNotImplemented, args[0])
}
},
Args: func(cmd *cobra.Command, args []string) error {
return nil
},
}
deployChallengeCmd = &cobra.Command{
Use: "deploy_challenge [flags] domain-name challenge-token-ignored challenge-contents",
Short: "Deploy ACME challenge",
Long: `
deploy_challenge creates the requested ACME DNS-01 TXT record for the given domain.
This is usually called directly by dehydrated.
Options that may be set using environment variables lists the matching variable names in square brackets.
`,
Example: `acme4netvs_dehydrated deploy_challenge --quiet example.kit.edu xxx NzczMGY1NmE3MDRhMDVmNWNmYzI3Mjk3ODI3NzQ3ODAgIC0K`,
Args: cobra.MatchAll(cobra.ExactArgs(3)),
Run: func(cmd *cobra.Command, args []string) {
apiClient = acme4netvs.BuildLocalNetVSClient(DebugPrintf)
fqdn, token := args[0], args[2]
err := acme4netvs.DeployChallenge(apiClient, fqdn, token, DebugPrintf)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeDeployment, fqdn, err)
}
if viper.GetBool("dns-wait") {
acme4netvs.CheckChallengeOnAllNS(
token,
fqdn,
viper.GetDuration("dns-wait-timeout"),
viper.GetDuration("dns-wait-between"),
DebugPrintf,
)
}
},
}
cleanChallengeCmd = &cobra.Command{
Use: "clean_challenge [flags] domain-name challenge-token-ignored challenge-contents",
Short: "Cleanup ACME challenge",
Long: `
clean_challenge removes the TXT record that was created by deploy_challenge.
This is usually called directly by dehydrated.
Options that may be set using environment variables lists the matching variable names in square brackets.
`,
Example: `acme4netvs_dehydrated clean_challenge --apikey 12345.sdfsfcdfcsd… example.kit.edu xxx NzczMGY1NmE3MDRhMDVmNWNmYzI3Mjk3ODI3NzQ3ODAgIC0K`,
Args: cobra.MatchAll(cobra.ExactArgs(3)),
Run: func(cmd *cobra.Command, args []string) {
apiClient = acme4netvs.BuildLocalNetVSClient(DebugPrintf)
fqdn, token := args[0], args[2]
err := acme4netvs.CleanChallenge(apiClient, fqdn, token, DebugPrintf)
if err != nil {
log.Fatalf(acme4netvs.FormatChallengeCleanup, fqdn, err)
}
DebugPrintf(acme4netvs.FormatChallengeCleanedUp, fqdn, token)
},
}
// cobra has no way to accept arbitrary commands so this list should capture everything dehydrated my throw at us
/* everythingElseCmd = &cobra.Command{
Use: "",
Aliases: []string{
"this_hookscript_is_broken__dehydrated_is_working_fine__please_ignore_unknown_hooks_in_your_script",
"deploy_cert",
"invalid_challenge",
"exit_hook",
"sync_cert",
"startup_hook",
"generate_csr",
"unchanged_cert",
"deploy_ocsp",
"request_failure",
},
Hidden: true,
SilenceErrors: true,
SilenceUsage: true,
DisableFlagParsing: true,
DisableAutoGenTag: true,
DisableSuggestions: true,
}*/
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
rootCmd.AddCommand(deployChallengeCmd)
rootCmd.AddCommand(cleanChallengeCmd)
//rootCmd.AddCommand(everythingElseCmd)
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
acme4netvs.CommonViperSetup(rootCmd, deployChallengeCmd, cleanChallengeCmd)
acme4netvs.CommonGlobalFlags(ConfigName, rootCmd)
acme4netvs.CommonDNSWaitFlags(rootCmd)
DebugPrintf = acme4netvs.FinalizeViperSetup(rootCmd)
}
package main
import (
"fmt"
"git.scc.kit.edu/KIT-CA/acme4netvs/v2"
flag "github.com/spf13/pflag"
"log"
"os"
"path"
)
var (
config acme4netvs.ClientConfig
apiClient *acme4netvs.NETVSClient
DebugPrintf acme4netvs.DebugPrintfFunc
)
// goreleaser variables
var (
version = "unknown"
commit = "unknown"
date = "unknown"
builtBy = "unknown"
)
func init() {
// custom help message
flag.Usage = func() {
fmt.Fprint(os.Stderr, "Usage (this is usually called directly by dehydrated):\n\n")
fmt.Fprintf(os.Stderr, " %s [OPTIONS] OPERATION DOMAIN CHALLENGE_TOKEN DNS-01-TOKEN\n\n", path.Base(os.Args[0]))
fmt.Fprint(os.Stderr, "OPERATION: clean_challenge | deploy_challenge \n\n")
fmt.Fprint(os.Stderr, "OPTIONS:\n\n")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintf(os.Stderr, "Version %s build on %s (commit %s)\n", version, date, commit)
fmt.Fprintln(os.Stderr, acme4netvs.ReleaseURLText)
os.Exit(0)
}
// parse flags
acme4netvs.DefineCommonFlags(&config)
acme4netvs.DefineCommonWaitingFlags(&config)
flag.Parse()
DebugPrintf = acme4netvs.MakeDebugFunc(!config.Quiet, os.Stdout)
log.SetFlags(log.Llongfile)
// early exit on unknown command
// this is correct because this hook only implements clean_challenge and deploy_challenge
args := flag.Args()
if len(args) < 1 {
log.Fatalf(acme4netvs.FormatNotEnoughArguments, 4, len(args))
}
if args[0] != "clean_challenge" && args[0] != "deploy_challenge" {
DebugPrintf(acme4netvs.FormatCommandNotImplemented, args[1], args[0])
os.Exit(0)
}
// build NETVS client
apiClient = acme4netvs.CreateNETVSClient(&config, DebugPrintf)
DebugPrintf("🚀 NETVS client created\n")
}
import "git.scc.kit.edu/KIT-CA/acme4netvs/v2/cmd/dehydrated_netvs_hook/cmd"
func main() {
//$1 an operation name (clean_challenge, deploy_challenge, deploy_cert, invalid_challenge or request_failure) and some operands for that. For deploy_challenge
//$2 is the domain name for which the certificate is required,
//$3 is a "challenge token" (which is not needed for dns-01), and
//$4 is a token which needs to be inserted in a TXT record for the domain.
args := flag.Args()
switch args[0] {
case "deploy_challenge":
if len(args) < 4 {
log.Fatalf(acme4netvs.FormatNotEnoughArguments, 4, len(args))
}
fqdn, token := args[1], args[3]
err