From c174a0c257f2a99e9bee8e5e56fc84056921a8c9 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sun, 8 Feb 2026 00:03:29 +0100 Subject: [PATCH] feat: hook manager --- cmd/cmd_renew.go | 37 ++++--- cmd/cmd_run.go | 33 +++--- cmd/flags.go | 52 +++++++++- cmd/internal/hook/hook.go | 55 +--------- cmd/internal/hook/hook_test.go | 11 -- cmd/internal/hook/manager.go | 96 ++++++++++++++++++ cmd/internal/hook/manager_options.go | 62 ++++++++++++ cmd/internal/hook/manager_test.go | 144 +++++++++++++++++++++++++++ cmd/internal/hook/metdata.go | 67 +++++++++++++ cmd/internal/hook/metdata_test.go | 17 ++++ cmd/setup.go | 11 ++ 11 files changed, 489 insertions(+), 96 deletions(-) create mode 100644 cmd/internal/hook/manager.go create mode 100644 cmd/internal/hook/manager_options.go create mode 100644 cmd/internal/hook/manager_test.go create mode 100644 cmd/internal/hook/metdata.go create mode 100644 cmd/internal/hook/metdata_test.go diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index e891d019f..fa3b65939 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -61,11 +61,6 @@ func renew(ctx context.Context, cmd *cli.Command) error { certsStorage := storage.NewCertificatesStorage(cmd.String(flgPath)) - meta := map[string]string{ - // TODO(ldez) add account ID. - hook.EnvAccountEmail: account.Email, - } - lazyClient := sync.OnceValues(func() (*lego.Client, error) { client, err := newClient(cmd, account, keyType) if err != nil { @@ -77,16 +72,18 @@ func renew(ctx context.Context, cmd *cli.Command) error { return client, nil }) + hookManager := newHookManager(cmd, certsStorage, account) + // CSR if cmd.IsSet(flgCSR) { - return renewForCSR(ctx, cmd, lazyClient, certsStorage, meta) + return renewForCSR(ctx, cmd, lazyClient, certsStorage, hookManager) } // Domains - return renewForDomains(ctx, cmd, lazyClient, certsStorage, meta) + return renewForDomains(ctx, cmd, lazyClient, certsStorage, hookManager) } -func renewForDomains(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, certsStorage *storage.CertificatesStorage, meta map[string]string) error { +func renewForDomains(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, certsStorage *storage.CertificatesStorage, hookManager *hook.Manager) error { domains := cmd.StringSlice(flgDomains) certID := cmd.String(flgCertName) @@ -147,6 +144,13 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, slog.Any("time-remaining", FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), ) + err = hookManager.Pre(ctx, certID, renewalDomains) + if err != nil { + return fmt.Errorf("pre-renew hook: %w", err) + } + + defer func() { _ = hookManager.Post(ctx) }() + client, err := lazyClient() if err != nil { return fmt.Errorf("set up client: %w", err) @@ -181,12 +185,10 @@ func renewForDomains(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, return fmt.Errorf("could not save the resource: %w", err) } - hook.AddPathToMetadata(meta, certRes, certsStorage, options) - - return hook.Launch(ctx, cmd.String(flgDeployHook), cmd.Duration(flgDeployHookTimeout), meta) + return hookManager.Deploy(ctx, certRes, options) } -func renewForCSR(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, certsStorage *storage.CertificatesStorage, meta map[string]string) error { +func renewForCSR(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, certsStorage *storage.CertificatesStorage, hookManager *hook.Manager) error { csr, err := readCSRFile(cmd.String(flgCSR)) if err != nil { return fmt.Errorf("could not read CSR file %q: %w", cmd.String(flgCSR), err) @@ -231,6 +233,13 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, cert slog.Any("time-remaining", FormattableDuration(cert.NotAfter.Sub(time.Now().UTC()))), ) + err = hookManager.Pre(ctx, certID, certcrypto.ExtractDomainsCSR(csr)) + if err != nil { + return fmt.Errorf("CSR: pre-renew hook: %w", err) + } + + defer func() { _ = hookManager.Post(ctx) }() + client, err := lazyClient() if err != nil { return fmt.Errorf("set up client: %w", err) @@ -256,9 +265,7 @@ func renewForCSR(ctx context.Context, cmd *cli.Command, lazyClient lzSetUp, cert return fmt.Errorf("CSR: could not save the resource: %w", err) } - hook.AddPathToMetadata(meta, certRes, certsStorage, options) - - return hook.Launch(ctx, cmd.String(flgDeployHook), cmd.Duration(flgDeployHookTimeout), meta) + return hookManager.Deploy(ctx, certRes, options) } func getFlagRenewDays(cmd *cli.Command) int { diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 4034e31ed..4c2a4305b 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -39,6 +39,10 @@ func run(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("set up account: %w", err) } + certsStorage := storage.NewCertificatesStorage(cmd.String(flgPath)) + + hookManager := newHookManager(cmd, certsStorage, account) + client, err := newClient(cmd, account, keyType) if err != nil { return fmt.Errorf("new client: %w", err) @@ -62,7 +66,7 @@ func run(ctx context.Context, cmd *cli.Command) error { setupChallenges(cmd, client) - certRes, err := obtainCertificate(ctx, cmd, client) + certRes, err := obtainCertificate(ctx, cmd, client, hookManager) if err != nil { // Make sure to return a non-zero exit code if ObtainSANCertificate returned at least one error. // Due to us not returning partial certificate we can just exit here instead of at the end. @@ -74,8 +78,6 @@ func run(ctx context.Context, cmd *cli.Command) error { certRes.ID = certID } - certsStorage := storage.NewCertificatesStorage(cmd.String(flgPath)) - options := newSaveOptions(cmd) err = certsStorage.Save(certRes, options) @@ -83,20 +85,20 @@ func run(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("could not save the resource: %w", err) } - meta := map[string]string{ - // TODO(ldez) add account ID. - hook.EnvAccountEmail: account.Email, - } - - hook.AddPathToMetadata(meta, certRes, certsStorage, options) - - return hook.Launch(ctx, cmd.String(flgDeployHook), cmd.Duration(flgDeployHookTimeout), meta) + return hookManager.Deploy(ctx, certRes, options) } -func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Client) (*certificate.Resource, error) { +func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Client, hookManager *hook.Manager) (*certificate.Resource, error) { domains := cmd.StringSlice(flgDomains) if len(domains) > 0 { + err := hookManager.Pre(ctx, cmd.String(flgCertName), domains) + if err != nil { + return nil, err + } + + defer func() { _ = hookManager.Post(ctx) }() + // obtain a certificate, generating a new private key request := newObtainRequest(cmd, domains) @@ -119,6 +121,13 @@ func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Clien return nil, err } + err = hookManager.Pre(ctx, cmd.String(flgCertName), certcrypto.ExtractDomainsCSR(csr)) + if err != nil { + return nil, err + } + + defer func() { _ = hookManager.Post(ctx) }() + // obtain a certificate for this CSR request := newObtainForCSRRequest(cmd, csr) diff --git a/cmd/flags.go b/cmd/flags.go index f59d423b3..53ac9cb54 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -143,8 +143,12 @@ const ( // Flags names related to hooks. const ( + flgPreHook = "pre-hook" + flgPreHookTimeout = "pre-hook-timeout" flgDeployHook = "deploy-hook" flgDeployHookTimeout = "deploy-hook-timeout" + flgPostHook = "post-hook" + flgPostHookTimeout = "post-hook-timeout" ) // Flag names related to logs. @@ -614,13 +618,31 @@ func createObtainFlags() []cli.Flag { } } -func createHookFlags() []cli.Flag { +func createPreHookFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Category: categoryHooks, + Name: flgPreHook, + Sources: cli.EnvVars(toEnvName(flgPreHook)), + Usage: "Define a pre-hook. This hook is runs, before the renewal, in cases where a certificate will be effectively renewed.", + }, + &cli.DurationFlag{ + Category: categoryHooks, + Name: flgPreHookTimeout, + Sources: cli.EnvVars(toEnvName(flgPreHookTimeout)), + Usage: "Define the timeout for the pre-hook execution.", + Value: 2 * time.Minute, + }, + } +} + +func createDeployHookFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ Category: categoryHooks, Name: flgDeployHook, Sources: cli.EnvVars(toEnvName(flgDeployHook)), - Usage: "Define a hook. The hook is executed only when the certificates are effectively created/renewed.", + Usage: "Define a hook. The hook is runs, after the renewal, in cases where a certificate is successfully created/renewed.", }, &cli.DurationFlag{ Category: categoryHooks, @@ -632,6 +654,24 @@ func createHookFlags() []cli.Flag { } } +func createPostHookFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Category: categoryHooks, + Name: flgPostHook, + Sources: cli.EnvVars(toEnvName(flgPostHook)), + Usage: "Define a post-hook. This hook runs, after the renewal, in cases where a certificate renewed, regardless of whether any errors occurred.", + }, + &cli.DurationFlag{ + Category: categoryHooks, + Name: flgPostHookTimeout, + Sources: cli.EnvVars(toEnvName(flgPostHookTimeout)), + Usage: "Define the timeout for the post-hook execution.", + Value: 2 * time.Minute, + }, + } +} + func CreateLogFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ @@ -663,7 +703,9 @@ func createRunFlags() []cli.Flag { flags = append(flags, createAcceptFlag()) flags = append(flags, createChallengesFlags()...) flags = append(flags, createObtainFlags()...) - flags = append(flags, createHookFlags()...) + flags = append(flags, createPreHookFlags()...) + flags = append(flags, createDeployHookFlags()...) + flags = append(flags, createPostHookFlags()...) flags = append(flags, &cli.StringFlag{ @@ -688,7 +730,9 @@ func createRenewFlags() []cli.Flag { flags = append(flags, createStorageFlags()...) flags = append(flags, createChallengesFlags()...) flags = append(flags, createObtainFlags()...) - flags = append(flags, createHookFlags()...) + flags = append(flags, createPreHookFlags()...) + flags = append(flags, createDeployHookFlags()...) + flags = append(flags, createPostHookFlags()...) flags = append(flags, &cli.IntFlag{ diff --git a/cmd/internal/hook/hook.go b/cmd/internal/hook/hook.go index d31946887..cb384ce13 100644 --- a/cmd/internal/hook/hook.go +++ b/cmd/internal/hook/hook.go @@ -9,29 +9,9 @@ import ( "os/exec" "strings" "time" - - "github.com/go-acme/lego/v5/certificate" - "github.com/go-acme/lego/v5/cmd/internal/storage" -) - -// TODO(ldez) rename the env vars with LEGO_HOOK_ prefix to avoid collisions with flag names. -const ( - EnvAccountEmail = "LEGO_ACCOUNT_EMAIL" - EnvCertDomain = "LEGO_CERT_DOMAIN" - EnvCertPath = "LEGO_CERT_PATH" - EnvCertKeyPath = "LEGO_CERT_KEY_PATH" - EnvIssuerCertKeyPath = "LEGO_ISSUER_CERT_PATH" - EnvCertPEMPath = "LEGO_CERT_PEM_PATH" - EnvCertPFXPath = "LEGO_CERT_PFX_PATH" -) - -// TODO(ldez): merge this with the previous constant block. -const ( - EnvCertNameSanitized = "LEGO_HOOK_CERT_NAME_SANITIZED" - EnvCertID = "LEGO_HOOK_CERT_ID" - EnvCertDomains = "LEGO_HOOK_CERT_DOMAINS" ) +// Launch executes a command. func Launch(ctx context.Context, hook string, timeout time.Duration, meta map[string]string) error { if hook == "" { return nil @@ -83,36 +63,3 @@ func Launch(ctx context.Context, hook string, timeout time.Duration, meta map[st return nil } - -func metaToEnv(meta map[string]string) []string { - var envs []string - - for k, v := range meta { - envs = append(envs, k+"="+v) - } - - return envs -} - -// AddPathToMetadata adds information about the certificate to the metadata map. -func AddPathToMetadata(meta map[string]string, certRes *certificate.Resource, certsStorage *storage.CertificatesStorage, options *storage.SaveOptions) { - meta[EnvCertID] = certRes.ID - meta[EnvCertNameSanitized] = storage.SanitizedName(certRes.ID) - - meta[EnvCertDomains] = strings.Join(certRes.Domains, ",") - - meta[EnvCertPath] = certsStorage.GetFileName(certRes.ID, storage.ExtCert) - meta[EnvCertKeyPath] = certsStorage.GetFileName(certRes.ID, storage.ExtKey) - - if certRes.IssuerCertificate != nil { - meta[EnvIssuerCertKeyPath] = certsStorage.GetFileName(certRes.ID, storage.ExtIssuer) - } - - if options.PEM { - meta[EnvCertPEMPath] = certsStorage.GetFileName(certRes.ID, storage.ExtPEM) - } - - if options.PFX { - meta[EnvCertPFXPath] = certsStorage.GetFileName(certRes.ID, storage.ExtPFX) - } -} diff --git a/cmd/internal/hook/hook_test.go b/cmd/internal/hook/hook_test.go index 26dae4762..76ba2e259 100644 --- a/cmd/internal/hook/hook_test.go +++ b/cmd/internal/hook/hook_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -60,13 +59,3 @@ func Test_Launch_errors(t *testing.T) { }) } } - -func Test_metaToEnv(t *testing.T) { - env := metaToEnv(map[string]string{ - "foo": "bar", - }) - - expected := []string{"foo=bar"} - - assert.Equal(t, expected, env) -} diff --git a/cmd/internal/hook/manager.go b/cmd/internal/hook/manager.go new file mode 100644 index 000000000..5b77a31a1 --- /dev/null +++ b/cmd/internal/hook/manager.go @@ -0,0 +1,96 @@ +package hook + +import ( + "context" + "fmt" + "time" + + "github.com/go-acme/lego/v5/certificate" + "github.com/go-acme/lego/v5/cmd/internal/storage" + "github.com/go-acme/lego/v5/log" +) + +// Action represents a hook action. +type Action struct { + Cmd string + Timeout time.Duration +} + +// Manager manages hooks. +type Manager struct { + certsStorage *storage.CertificatesStorage + + metadata map[string]string + + pre *Action + deploy *Action + post *Action +} + +// NewManager creates a new hook Manager. +func NewManager(certsStorage *storage.CertificatesStorage, options ...Option) *Manager { + m := &Manager{ + certsStorage: certsStorage, + metadata: make(map[string]string), + } + + for _, option := range options { + option(m) + } + + return m +} + +// Pre runs the pre-hook if defined. +func (h *Manager) Pre(ctx context.Context, certID string, domains []string) error { + if h.pre == nil || h.pre.Cmd == "" { + return nil + } + + addCertificateMetadata(h.metadata, certID, domains) + + err := Launch(ctx, h.pre.Cmd, h.pre.Timeout, h.metadata) + if err != nil { + log.Error("Pre hook.", log.ErrorAttr(err)) + + return fmt.Errorf("pre hook: %w", err) + } + + return nil +} + +// Deploy runs the deploy-hook if defined. +func (h *Manager) Deploy(ctx context.Context, certRes *certificate.Resource, options *storage.SaveOptions) error { + if h.deploy == nil || h.deploy.Cmd == "" { + return nil + } + + addCertificateMetadata(h.metadata, certRes.ID, certRes.Domains) + addCertificatePathsMetadata(h.metadata, certRes, h.certsStorage, options) + + err := Launch(ctx, h.deploy.Cmd, h.deploy.Timeout, h.metadata) + if err != nil { + log.Error("Deploy hook.", log.ErrorAttr(err)) + + return fmt.Errorf("deploy hook: %w", err) + } + + return nil +} + +// Post runs the post-hook if defined. +// This must be called inside a defer statement to ensure the hook is always run. +func (h *Manager) Post(ctx context.Context) error { + if h.post == nil || h.post.Cmd == "" { + return nil + } + + err := Launch(ctx, h.post.Cmd, h.post.Timeout, h.metadata) + if err != nil { + log.Error("Post hook.", log.ErrorAttr(err)) + + return fmt.Errorf("post hook: %w", err) + } + + return nil +} diff --git a/cmd/internal/hook/manager_options.go b/cmd/internal/hook/manager_options.go new file mode 100644 index 000000000..a051279e6 --- /dev/null +++ b/cmd/internal/hook/manager_options.go @@ -0,0 +1,62 @@ +package hook + +import ( + "time" + + "github.com/go-acme/lego/v5/cmd/internal/storage" +) + +type Option func(m *Manager) + +// WithPre sets the pre-hook. +func WithPre(cmd string, timeout time.Duration) Option { + return func(m *Manager) { + if cmd == "" { + return + } + + m.pre = &Action{ + Cmd: cmd, + Timeout: timeout, + } + } +} + +// WithDeploy sets the deploy-hook. +func WithDeploy(cmd string, timeout time.Duration) Option { + return func(m *Manager) { + if cmd == "" { + return + } + + m.deploy = &Action{ + Cmd: cmd, + Timeout: timeout, + } + } +} + +// WithPost sets the post-hook. +func WithPost(cmd string, timeout time.Duration) Option { + return func(m *Manager) { + if cmd == "" { + return + } + + m.post = &Action{ + Cmd: cmd, + Timeout: timeout, + } + } +} + +// WithAccountMetadata initializes the metadata with the account data. +func WithAccountMetadata(account *storage.Account) Option { + return func(m *Manager) { + if account == nil { + return + } + + addAccountMetadata(m.metadata, account) + } +} diff --git a/cmd/internal/hook/manager_test.go b/cmd/internal/hook/manager_test.go new file mode 100644 index 000000000..62c61e655 --- /dev/null +++ b/cmd/internal/hook/manager_test.go @@ -0,0 +1,144 @@ +package hook + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v5/certificate" + "github.com/go-acme/lego/v5/cmd/internal/storage" + "github.com/stretchr/testify/require" +) + +func Test_Manager(t *testing.T) { + certificatesStorage := storage.NewCertificatesStorage(t.TempDir()) + + testCases := []struct { + desc string + options []Option + }{ + { + desc: "all hooks", + options: []Option{ + WithPre("echo Pre Hook", 1*time.Second), + WithDeploy("echo Deploy Hook", 1*time.Second), + WithPost("echo Post Hook", 1*time.Second), + }, + }, + { + desc: "pre-hook only", + options: []Option{ + WithPre("echo Pre Hook", 1*time.Second), + }, + }, + { + desc: "deploy-hook only", + options: []Option{ + WithDeploy("echo Deploy Hook", 1*time.Second), + }, + }, + { + desc: "post-hook only", + options: []Option{ + WithPost("echo Post Hook", 1*time.Second), + }, + }, + { + desc: "no hook", + }, + { + desc: "all hooks (metadata)", + options: []Option{ + WithPre("echo Pre Hook", 1*time.Second), + WithDeploy("echo Deploy Hook", 1*time.Second), + WithPost("echo Post Hook", 1*time.Second), + WithAccountMetadata(&storage.Account{ID: "foo@exmaple.com", Email: "bar@example.com"}), + }, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + manager := NewManager(certificatesStorage, test.options...) + + err := manager.Pre(t.Context(), "a", []string{"example.com", "example.org"}) + require.NoError(t, err) + + t.Log(manager.metadata) + + err = manager.Deploy(t.Context(), &certificate.Resource{ID: "example.org"}, &storage.SaveOptions{}) + require.NoError(t, err) + + t.Log(manager.metadata) + + err = manager.Post(t.Context()) + require.NoError(t, err) + + t.Log(manager.metadata) + }) + } +} + +func Test_Manager_errors(t *testing.T) { + certificatesStorage := storage.NewCertificatesStorage(t.TempDir()) + + testCases := []struct { + desc string + options []Option + requirePre require.ErrorAssertionFunc + requireDeploy require.ErrorAssertionFunc + requirePost require.ErrorAssertionFunc + }{ + { + desc: "pre-hook error", + options: []Option{ + WithPre("thisappdoesnotexistpre", 1*time.Second), + WithDeploy("echo Deploy Hook", 1*time.Second), + WithPost("echo Post Hook", 1*time.Second), + }, + requirePre: require.Error, + requireDeploy: require.NoError, + requirePost: require.NoError, + }, + { + desc: "deploy-hook error", + options: []Option{ + WithPre("echo Pre Hook", 1*time.Second), + WithDeploy("thiscommanddoesnotexistdeploy", 1*time.Second), + WithPost("echo Post Hook", 1*time.Second), + }, + requirePre: require.NoError, + requireDeploy: require.Error, + requirePost: require.NoError, + }, + { + desc: "post-hook error", + options: []Option{ + WithPre("echo Pre Hook", 1*time.Second), + WithDeploy("echo Deploy Hook", 1*time.Second), + WithPost("thiscommanddoesnotexistpost", 1*time.Second), + }, + requirePre: require.NoError, + requireDeploy: require.NoError, + requirePost: require.Error, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + manager := NewManager(certificatesStorage, test.options...) + + err := manager.Pre(t.Context(), "a", []string{"example.com", "example.org"}) + test.requirePre(t, err) + + err = manager.Deploy(t.Context(), &certificate.Resource{ID: "example.org"}, &storage.SaveOptions{}) + test.requireDeploy(t, err) + + err = manager.Post(t.Context()) + test.requirePost(t, err) + }) + } +} diff --git a/cmd/internal/hook/metdata.go b/cmd/internal/hook/metdata.go new file mode 100644 index 000000000..63ccad813 --- /dev/null +++ b/cmd/internal/hook/metdata.go @@ -0,0 +1,67 @@ +package hook + +import ( + "strings" + + "github.com/go-acme/lego/v5/certificate" + "github.com/go-acme/lego/v5/cmd/internal/storage" +) + +// Metadata related to account. +const ( + EnvAccountID = "LEGO_HOOK_ACCOUNT_ID" + EnvAccountEmail = "LEGO_HOOK_ACCOUNT_EMAIL" +) + +// Metadata related to certificate. +const ( + EnvCertName = "LEGO_HOOK_CERT_NAME" + EnvCertNameSanitized = "LEGO_HOOK_CERT_NAME_SANITIZED" + EnvCertDomains = "LEGO_HOOK_CERT_DOMAINS" + EnvCertPath = "LEGO_HOOK_CERT_PATH" + EnvCertKeyPath = "LEGO_HOOK_CERT_KEY_PATH" + EnvIssuerCertKeyPath = "LEGO_HOOK_ISSUER_CERT_PATH" + EnvCertPEMPath = "LEGO_HOOK_CERT_PEM_PATH" + EnvCertPFXPath = "LEGO_HOOK_CERT_PFX_PATH" +) + +func addAccountMetadata(meta map[string]string, account *storage.Account) { + meta[EnvAccountID] = account.ID + meta[EnvAccountEmail] = account.Email +} + +func addCertificatePathsMetadata(meta map[string]string, certRes *certificate.Resource, certsStorage *storage.CertificatesStorage, options *storage.SaveOptions) { + meta[EnvCertPath] = certsStorage.GetFileName(certRes.ID, storage.ExtCert) + meta[EnvCertKeyPath] = certsStorage.GetFileName(certRes.ID, storage.ExtKey) + + if certRes.IssuerCertificate != nil { + meta[EnvIssuerCertKeyPath] = certsStorage.GetFileName(certRes.ID, storage.ExtIssuer) + } + + if options.PEM { + meta[EnvCertPEMPath] = certsStorage.GetFileName(certRes.ID, storage.ExtPEM) + } + + if options.PFX { + meta[EnvCertPFXPath] = certsStorage.GetFileName(certRes.ID, storage.ExtPFX) + } +} + +func addCertificateMetadata(meta map[string]string, certID string, domains []string) { + if certID == "" { + meta[EnvCertName] = certID + meta[EnvCertNameSanitized] = storage.SanitizedName(certID) + } + + meta[EnvCertDomains] = strings.Join(domains, ",") +} + +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/internal/hook/metdata_test.go b/cmd/internal/hook/metdata_test.go new file mode 100644 index 000000000..f29bcf3c9 --- /dev/null +++ b/cmd/internal/hook/metdata_test.go @@ -0,0 +1,17 @@ +package hook + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_metaToEnv(t *testing.T) { + env := metaToEnv(map[string]string{ + "foo": "bar", + }) + + expected := []string{"foo=bar"} + + assert.Equal(t, expected, env) +} diff --git a/cmd/setup.go b/cmd/setup.go index 0feef489c..4db01980f 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -17,6 +17,7 @@ import ( "github.com/go-acme/lego/v5/acme" "github.com/go-acme/lego/v5/certcrypto" "github.com/go-acme/lego/v5/certificate" + "github.com/go-acme/lego/v5/cmd/internal/hook" "github.com/go-acme/lego/v5/cmd/internal/storage" "github.com/go-acme/lego/v5/lego" "github.com/go-acme/lego/v5/log" @@ -204,6 +205,16 @@ func newSaveOptions(cmd *cli.Command) *storage.SaveOptions { } } +func newHookManager(cmd *cli.Command, certsStorage *storage.CertificatesStorage, account *storage.Account) *hook.Manager { + return hook.NewManager( + certsStorage, + hook.WithPre(cmd.String(flgPreHook), cmd.Duration(flgPreHookTimeout)), + hook.WithDeploy(cmd.String(flgDeployHook), cmd.Duration(flgDeployHookTimeout)), + hook.WithPost(cmd.String(flgPostHook), cmd.Duration(flgPostHookTimeout)), + hook.WithAccountMetadata(account), + ) +} + func parseAddress(cmd *cli.Command, flgName string) (string, string, error) { address := cmd.String(flgName)