From e07bf641ab54e858b3ad0e5ed7a0efe13f54b60b Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 14 May 2020 23:44:08 +0200 Subject: [PATCH] feat: add hook on the run command. (#1157) --- cmd/cmd_renew.go | 47 ++---------------------------- cmd/cmd_run.go | 13 ++++++++- cmd/hook.go | 47 ++++++++++++++++++++++++++++++ docs/content/usage/cli/_index.md | 4 +-- docs/content/usage/cli/examples.md | 15 ++++++++++ 5 files changed, 78 insertions(+), 48 deletions(-) create mode 100644 cmd/hook.go diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index 0d82c968..72cd6f6e 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -1,14 +1,8 @@ package cmd import ( - "context" "crypto" "crypto/x509" - "errors" - "fmt" - "os" - "os/exec" - "strings" "time" "github.com/go-acme/lego/v3/certcrypto" @@ -145,7 +139,7 @@ func renewForDomains(ctx *cli.Context, client *lego.Client, certsStorage *Certif meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt") meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key") - return renewHook(ctx, meta) + return launchHook(ctx.String("renew-hook"), meta) } func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { @@ -185,7 +179,7 @@ func renewForCSR(ctx *cli.Context, client *lego.Client, certsStorage *Certificat meta[renewEnvCertPath] = certsStorage.GetFileName(domain, ".crt") meta[renewEnvCertKeyPath] = certsStorage.GetFileName(domain, ".key") - return renewHook(ctx, meta) + return launchHook(ctx.String("renew-hook"), meta) } func needRenewal(x509Cert *x509.Certificate, domain string, days int) bool { @@ -220,40 +214,3 @@ func merge(prevDomains []string, nextDomains []string) []string { } return prevDomains } - -func renewHook(ctx *cli.Context, meta map[string]string) error { - hook := ctx.String("renew-hook") - if hook == "" { - return nil - } - - ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second) - defer cancel() - - parts := strings.Fields(hook) - - cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) - cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...) - - output, err := cmdCtx.CombinedOutput() - - if len(output) > 0 { - fmt.Println(string(output)) - } - - if ctxCmd.Err() == context.DeadlineExceeded { - return errors.New("hook timed out") - } - - return err -} - -func metaToEnv(meta map[string]string) []string { - var envs []string - - for k, v := range meta { - envs = append(envs, k+"="+v) - } - - return envs -} diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index e71437f2..4a869fd4 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -39,6 +39,10 @@ func createRun() cli.Command { Name: "must-staple", Usage: "Include the OCSP must staple TLS extension in the CSR and generated certificate. Only works if the CSR is generated by lego.", }, + cli.StringFlag{ + Name: "run-hook", + Usage: "Define a hook. The hook is executed when the certificates are effectively created.", + }, }, } } @@ -86,7 +90,14 @@ func run(ctx *cli.Context) error { certsStorage.SaveResource(cert) - return nil + meta := map[string]string{ + renewEnvAccountEmail: account.Email, + renewEnvCertDomain: cert.Domain, + renewEnvCertPath: certsStorage.GetFileName(cert.Domain, ".crt"), + renewEnvCertKeyPath: certsStorage.GetFileName(cert.Domain, ".key"), + } + + return launchHook(ctx.String("run-hook"), meta) } func handleTOS(ctx *cli.Context, client *lego.Client) bool { diff --git a/cmd/hook.go b/cmd/hook.go new file mode 100644 index 00000000..0aef6338 --- /dev/null +++ b/cmd/hook.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +func launchHook(hook string, meta map[string]string) error { + if hook == "" { + return nil + } + + ctxCmd, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + parts := strings.Fields(hook) + + cmdCtx := exec.CommandContext(ctxCmd, parts[0], parts[1:]...) + cmdCtx.Env = append(os.Environ(), metaToEnv(meta)...) + + output, err := cmdCtx.CombinedOutput() + + if len(output) > 0 { + fmt.Println(string(output)) + } + + if ctxCmd.Err() == context.DeadlineExceeded { + return errors.New("hook timed out") + } + + return err +} + +func metaToEnv(meta map[string]string) []string { + var envs []string + + for k, v := range meta { + envs = append(envs, k+"="+v) + } + + return envs +} diff --git a/docs/content/usage/cli/_index.md b/docs/content/usage/cli/_index.md index 2ea64fe1..9a91c9b3 100644 --- a/docs/content/usage/cli/_index.md +++ b/docs/content/usage/cli/_index.md @@ -37,11 +37,11 @@ GLOBAL OPTIONS: --hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding. --key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384. (default: "ec384") --filename value (deprecated) Filename of the generated certificate. - --path value Directory to use for storing the data. (default: "./.lego") + --path value Directory to use for storing the data. (default: "./.lego") [$LEGO_PATH] --http Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges. --http.port value Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port. (default: ":80") --http.proxy-header value Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy. (default: "Host") - --http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be served at /.well-known/acme-challenge + --http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts. --tls Use the TLS challenge to solve challenges. Can be mixed with other types of challenges. --tls.port value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port. (default: ":443") diff --git a/docs/content/usage/cli/examples.md b/docs/content/usage/cli/examples.md index eb77fe8f..55bc140d 100644 --- a/docs/content/usage/cli/examples.md +++ b/docs/content/usage/cli/examples.md @@ -18,6 +18,21 @@ lego --email="foo@bar.com" --domains="example.com" --http run (Find your certificate in the `.lego` folder of current working directory.) +### Obtain a certificate (and hook) + +The hook is executed only when the certificates are effectively created. + +```bash +lego --email="foo@bar.com" --domains="example.com" --http run --run-hook="./myscript.sh" +``` + +Some information are added to the environment variables when the hook is used: + +- `LEGO_ACCOUNT_EMAIL`: the email of the account. +- `LEGO_CERT_DOMAIN`: the main domain of the certificate. +- `LEGO_CERT_PATH`: the path of the certificate. +- `LEGO_CERT_KEY_PATH`: the path of the certificate key. + ### To renew the certificate ```bash