From b9de485eed962f3e38ed3dd5704e3a02f3c2c0e5 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Mon, 19 Jan 2026 23:28:48 +0100 Subject: [PATCH] chore: migrate to urfave/cli v3 --- cmd/accounts_storage.go | 29 ++++----- cmd/certs_storage.go | 18 +++--- cmd/cmd.go | 2 +- cmd/cmd_before.go | 15 ++--- cmd/cmd_dnshelp.go | 11 ++-- cmd/cmd_list.go | 21 ++++--- cmd/cmd_renew.go | 115 ++++++++++++++++++---------------- cmd/cmd_revoke.go | 20 +++--- cmd/cmd_run.go | 103 +++++++++++++++--------------- cmd/flags.go | 30 +++------ cmd/lego/main.go | 32 +++++----- cmd/setup.go | 42 ++++++------- cmd/setup_challenges.go | 90 +++++++++++++------------- go.mod | 5 +- go.sum | 10 +-- internal/clihelp/generator.go | 33 +++++----- internal/releaser/releaser.go | 69 ++++++++++---------- 17 files changed, 324 insertions(+), 321 deletions(-) diff --git a/cmd/accounts_storage.go b/cmd/accounts_storage.go index 2d765b845..5ce6828de 100644 --- a/cmd/accounts_storage.go +++ b/cmd/accounts_storage.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "crypto" "encoding/json" "encoding/pem" @@ -13,7 +14,7 @@ import ( "github.com/go-acme/lego/v5/lego" "github.com/go-acme/lego/v5/log" "github.com/go-acme/lego/v5/registration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const userIDPlaceholder = "noemail@example.com" @@ -64,25 +65,25 @@ type AccountsStorage struct { rootUserPath string keysPath string accountFilePath string - ctx *cli.Context + cmd *cli.Command } // NewAccountsStorage Creates a new AccountsStorage. -func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { +func NewAccountsStorage(cmd *cli.Command) *AccountsStorage { // TODO: move to account struct? - email := ctx.String(flgEmail) + email := cmd.String(flgEmail) userID := email if userID == "" { userID = userIDPlaceholder } - serverURL, err := url.Parse(ctx.String(flgServer)) + serverURL, err := url.Parse(cmd.String(flgServer)) if err != nil { - log.Fatal("URL parsing", "flag", flgServer, "serverURL", ctx.String(flgServer), "error", err) + log.Fatal("URL parsing", "flag", flgServer, "serverURL", cmd.String(flgServer), "error", err) } - rootPath := filepath.Join(ctx.String(flgPath), baseAccountsRootFolderName) + rootPath := filepath.Join(cmd.String(flgPath), baseAccountsRootFolderName) serverPath := strings.NewReplacer(":", "_", "/", string(os.PathSeparator)).Replace(serverURL.Host) accountsPath := filepath.Join(rootPath, serverPath) rootUserPath := filepath.Join(accountsPath, userID) @@ -94,7 +95,7 @@ func NewAccountsStorage(ctx *cli.Context) *AccountsStorage { rootUserPath: rootUserPath, keysPath: filepath.Join(rootUserPath, baseKeysFolderName), accountFilePath: filepath.Join(rootUserPath, accountFileName), - ctx: ctx, + cmd: cmd, } } @@ -134,7 +135,7 @@ func (s *AccountsStorage) Save(account *Account) error { return os.WriteFile(s.accountFilePath, jsonBytes, filePerm) } -func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { +func (s *AccountsStorage) LoadAccount(ctx context.Context, privateKey crypto.PrivateKey) *Account { fileBytes, err := os.ReadFile(s.accountFilePath) if err != nil { log.Fatal("Could not load the account file.", "userID", s.GetUserID(), "error", err) @@ -150,7 +151,7 @@ func (s *AccountsStorage) LoadAccount(privateKey crypto.PrivateKey) *Account { account.key = privateKey if account.Registration == nil || account.Registration.Body.Status == "" { - reg, err := tryRecoverRegistration(s.ctx, privateKey) + reg, err := tryRecoverRegistration(ctx, s.cmd, privateKey) if err != nil { log.Fatal("Could not load the account file. Registration is nil.", "userID", s.GetUserID(), "error", err) } @@ -233,18 +234,18 @@ func loadPrivateKey(file string) (crypto.PrivateKey, error) { return privateKey, nil } -func tryRecoverRegistration(cliCtx *cli.Context, privateKey crypto.PrivateKey) (*registration.Resource, error) { +func tryRecoverRegistration(ctx context.Context, cmd *cli.Command, privateKey crypto.PrivateKey) (*registration.Resource, error) { // couldn't load account but got a key. Try to look the account up. config := lego.NewConfig(&Account{key: privateKey}) - config.CADirURL = cliCtx.String(flgServer) - config.UserAgent = getUserAgent(cliCtx) + config.CADirURL = cmd.String(flgServer) + config.UserAgent = getUserAgent(cmd) client, err := lego.NewClient(config) if err != nil { return nil, err } - reg, err := client.Registration.ResolveAccountByKey(cliCtx.Context) + reg, err := client.Registration.ResolveAccountByKey(ctx) if err != nil { return nil, err } diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go index 813ef8d44..1411a2c84 100644 --- a/cmd/certs_storage.go +++ b/cmd/certs_storage.go @@ -16,7 +16,7 @@ import ( "github.com/go-acme/lego/v5/certcrypto" "github.com/go-acme/lego/v5/certificate" "github.com/go-acme/lego/v5/log" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "golang.org/x/net/idna" "software.sslmate.com/src/go-pkcs12" ) @@ -59,8 +59,8 @@ type CertificatesStorage struct { } // NewCertificatesStorage create a new certificates storage. -func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { - pfxFormat := ctx.String(flgPFXFormat) +func NewCertificatesStorage(cmd *cli.Command) *CertificatesStorage { + pfxFormat := cmd.String(flgPFXFormat) switch pfxFormat { case "DES", "RC2", "SHA256": @@ -69,13 +69,13 @@ func NewCertificatesStorage(ctx *cli.Context) *CertificatesStorage { } return &CertificatesStorage{ - rootPath: filepath.Join(ctx.String(flgPath), baseCertificatesFolderName), - archivePath: filepath.Join(ctx.String(flgPath), baseArchivesFolderName), - pem: ctx.Bool(flgPEM), - pfx: ctx.Bool(flgPFX), - pfxPassword: ctx.String(flgPFXPass), + rootPath: filepath.Join(cmd.String(flgPath), baseCertificatesFolderName), + archivePath: filepath.Join(cmd.String(flgPath), baseArchivesFolderName), + pem: cmd.Bool(flgPEM), + pfx: cmd.Bool(flgPFX), + pfxPassword: cmd.String(flgPFXPass), pfxFormat: pfxFormat, - filename: ctx.String(flgFilename), + filename: cmd.String(flgFilename), } } diff --git a/cmd/cmd.go b/cmd/cmd.go index 4d4dd3afa..d34e5e3f6 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,6 +1,6 @@ package cmd -import "github.com/urfave/cli/v2" +import "github.com/urfave/cli/v3" // CreateCommands Creates all CLI commands. func CreateCommands() []*cli.Command { diff --git a/cmd/cmd_before.go b/cmd/cmd_before.go index 37fece5a9..34995964b 100644 --- a/cmd/cmd_before.go +++ b/cmd/cmd_before.go @@ -1,25 +1,26 @@ package cmd import ( + "context" "fmt" "github.com/go-acme/lego/v5/log" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -func Before(ctx *cli.Context) error { - if ctx.String(flgPath) == "" { +func Before(ctx context.Context, cmd *cli.Command) (context.Context, error) { + if cmd.String(flgPath) == "" { log.Fatal(fmt.Sprintf("Could not determine the current working directory. Please pass --%s.", flgPath)) } - err := createNonExistingFolder(ctx.String(flgPath)) + err := createNonExistingFolder(cmd.String(flgPath)) if err != nil { - log.Fatal("Could not check/create the path.", "flag", flgPath, "filepath", ctx.String(flgPath), "error", err) + log.Fatal("Could not check/create the path.", "flag", flgPath, "filepath", cmd.String(flgPath), "error", err) } - if ctx.String(flgServer) == "" { + if cmd.String(flgServer) == "" { log.Fatal(fmt.Sprintf("Could not determine the current working server. Please pass --%s.", flgServer)) } - return nil + return ctx, nil } diff --git a/cmd/cmd_dnshelp.go b/cmd/cmd_dnshelp.go index 41adf4c8d..e7b9ad85c 100644 --- a/cmd/cmd_dnshelp.go +++ b/cmd/cmd_dnshelp.go @@ -1,12 +1,13 @@ package cmd import ( + "context" "fmt" "io" "strings" "text/tabwriter" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const flgCode = "code" @@ -26,10 +27,10 @@ func createDNSHelp() *cli.Command { } } -func dnsHelp(ctx *cli.Context) error { - code := ctx.String(flgCode) +func dnsHelp(_ context.Context, cmd *cli.Command) error { + code := cmd.String(flgCode) if code == "" { - w := tabwriter.NewWriter(ctx.App.Writer, 0, 0, 2, ' ', 0) + w := tabwriter.NewWriter(cmd.Writer, 0, 0, 2, ' ', 0) ew := &errWriter{w: w} ew.writeln(`Credentials for DNS providers must be passed through environment variables.`) @@ -50,7 +51,7 @@ func dnsHelp(ctx *cli.Context) error { return w.Flush() } - return displayDNSHelp(ctx.App.Writer, strings.ToLower(code)) + return displayDNSHelp(cmd.Writer, strings.ToLower(code)) } type errWriter struct { diff --git a/cmd/cmd_list.go b/cmd/cmd_list.go index bd1b518a2..3d1de2588 100644 --- a/cmd/cmd_list.go +++ b/cmd/cmd_list.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "encoding/json" "fmt" "net/url" @@ -9,7 +10,7 @@ import ( "strings" "github.com/go-acme/lego/v5/certcrypto" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( @@ -43,25 +44,25 @@ func createList() *cli.Command { } } -func list(ctx *cli.Context) error { - if ctx.Bool(flgAccounts) && !ctx.Bool(flgNames) { - if err := listAccount(ctx); err != nil { +func list(ctx context.Context, cmd *cli.Command) error { + if cmd.Bool(flgAccounts) && !cmd.Bool(flgNames) { + if err := listAccount(ctx, cmd); err != nil { return err } } - return listCertificates(ctx) + return listCertificates(ctx, cmd) } -func listCertificates(ctx *cli.Context) error { - certsStorage := NewCertificatesStorage(ctx) +func listCertificates(_ context.Context, cmd *cli.Command) error { + certsStorage := NewCertificatesStorage(cmd) matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt")) if err != nil { return err } - names := ctx.Bool(flgNames) + names := cmd.Bool(flgNames) if len(matches) == 0 { if !names { @@ -109,8 +110,8 @@ func listCertificates(ctx *cli.Context) error { return nil } -func listAccount(ctx *cli.Context) error { - accountsStorage := NewAccountsStorage(ctx) +func listAccount(_ context.Context, cmd *cli.Command) error { + accountsStorage := NewAccountsStorage(cmd) matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json")) if err != nil { diff --git a/cmd/cmd_renew.go b/cmd/cmd_renew.go index aff22372d..a5a022a4b 100644 --- a/cmd/cmd_renew.go +++ b/cmd/cmd_renew.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "crypto" "crypto/x509" "errors" @@ -16,7 +17,7 @@ import ( "github.com/go-acme/lego/v5/lego" "github.com/go-acme/lego/v5/log" "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // Flag names. @@ -37,11 +38,11 @@ func createRenew() *cli.Command { Name: "renew", Usage: "Renew a certificate", Action: renew, - Before: func(ctx *cli.Context) error { + Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { // we require either domains or csr, but not both - hasDomains := len(ctx.StringSlice(flgDomains)) > 0 + hasDomains := len(cmd.StringSlice(flgDomains)) > 0 - hasCsr := ctx.String(flgCSR) != "" + hasCsr := cmd.String(flgCSR) != "" if hasDomains && hasCsr { log.Fatal(fmt.Sprintf("Please specify either --%s/-d or --%s/-c, but not both", flgDomains, flgCSR)) } @@ -50,11 +51,11 @@ func createRenew() *cli.Command { log.Fatal(fmt.Sprintf("Please specify --%s/-d (or --%s/-c if you already have a CSR)", flgDomains, flgCSR)) } - if ctx.Bool(flgForceCertDomains) && hasCsr { + if cmd.Bool(flgForceCertDomains) && hasCsr { log.Fatal(fmt.Sprintf("--%s only works with --%s/-d, --%s/-c doesn't support this option.", flgForceCertDomains, flgDomains, flgCSR)) } - return nil + return ctx, nil }, Flags: []cli.Flag{ &cli.IntFlag{ @@ -90,14 +91,18 @@ func createRenew() *cli.Command { " Only works if the CSR is generated by lego.", }, &cli.TimestampFlag{ - Name: flgNotBefore, - Usage: "Set the notBefore field in the certificate (RFC3339 format)", - Layout: time.RFC3339, + Name: flgNotBefore, + Usage: "Set the notBefore field in the certificate (RFC3339 format)", + Config: cli.TimestampConfig{ + Layouts: []string{time.RFC3339}, + }, }, &cli.TimestampFlag{ - Name: flgNotAfter, - Usage: "Set the notAfter field in the certificate (RFC3339 format)", - Layout: time.RFC3339, + Name: flgNotAfter, + Usage: "Set the notAfter field in the certificate (RFC3339 format)", + Config: cli.TimestampConfig{ + Layouts: []string{time.RFC3339}, + }, }, &cli.StringFlag{ Name: flgPreferredChain, @@ -134,32 +139,32 @@ func createRenew() *cli.Command { } } -func renew(cliCtx *cli.Context) error { - account, keyType := setupAccount(cliCtx, NewAccountsStorage(cliCtx)) +func renew(ctx context.Context, cmd *cli.Command) error { + account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd)) if account.Registration == nil { log.Fatal("The account is not registered. Use 'run' to register a new account.", "email", account.Email) } - certsStorage := NewCertificatesStorage(cliCtx) + certsStorage := NewCertificatesStorage(cmd) - bundle := !cliCtx.Bool(flgNoBundle) + bundle := !cmd.Bool(flgNoBundle) meta := map[string]string{ hookEnvAccountEmail: account.Email, } // CSR - if cliCtx.IsSet(flgCSR) { - return renewForCSR(cliCtx, account, keyType, certsStorage, bundle, meta) + if cmd.IsSet(flgCSR) { + return renewForCSR(ctx, cmd, account, keyType, certsStorage, bundle, meta) } // Domains - return renewForDomains(cliCtx, account, keyType, certsStorage, bundle, meta) + return renewForDomains(ctx, cmd, account, keyType, certsStorage, bundle, meta) } -func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { - domains := cliCtx.StringSlice(flgDomains) +func renewForDomains(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { + domains := cmd.StringSlice(flgDomains) domain := domains[0] // load the cert resource from files. @@ -179,10 +184,10 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K var client *lego.Client - if !cliCtx.Bool(flgARIDisable) { - client = setupClient(cliCtx, account, keyType) + if !cmd.Bool(flgARIDisable) { + client = setupClient(cmd, account, keyType) - ariRenewalTime = getARIRenewalTime(cliCtx, cert, domain, client) + ariRenewalTime = getARIRenewalTime(ctx, cmd, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() @@ -199,17 +204,17 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K } } - forceDomains := cliCtx.Bool(flgForceCertDomains) + forceDomains := cmd.Bool(flgForceCertDomains) certDomains := certcrypto.ExtractDomains(cert) - if ariRenewalTime == nil && !needRenewal(cert, domain, cliCtx.Int(flgRenewDays), cliCtx.Bool(flgRenewDynamic)) && + if ariRenewalTime == nil && !needRenewal(cert, domain, cmd.Int(flgRenewDays), cmd.Bool(flgRenewDynamic)) && (!forceDomains || slices.Equal(certDomains, domains)) { return nil } if client == nil { - client = setupClient(cliCtx, account, keyType) + client = setupClient(cmd, account, keyType) } // This is just meant to be informal for the user. @@ -218,7 +223,7 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K var privateKey crypto.PrivateKey - if cliCtx.Bool(flgReuseKey) { + if cmd.Bool(flgReuseKey) { keyBytes, errR := certsStorage.ReadFile(domain, keyExt) if errR != nil { log.Fatal("Error while loading the private key.", "domain", domain, "error", errR) @@ -232,7 +237,7 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K // https://github.com/go-acme/lego/issues/1656 // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L435-L440 - if !isatty.IsTerminal(os.Stdout.Fd()) && !cliCtx.Bool(flgNoRandomSleep) { + if !isatty.IsTerminal(os.Stdout.Fd()) && !cmd.Bool(flgNoRandomSleep) { // https://github.com/certbot/certbot/blob/284023a1b7672be2bd4018dd7623b3b92197d4b0/certbot/certbot/_internal/renewal.py#L472 const jitter = 8 * time.Minute @@ -251,20 +256,20 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K request := certificate.ObtainRequest{ Domains: renewalDomains, PrivateKey: privateKey, - MustStaple: cliCtx.Bool(flgMustStaple), - NotBefore: getTime(cliCtx, flgNotBefore), - NotAfter: getTime(cliCtx, flgNotAfter), + MustStaple: cmd.Bool(flgMustStaple), + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), Bundle: bundle, - PreferredChain: cliCtx.String(flgPreferredChain), - Profile: cliCtx.String(flgProfile), - AlwaysDeactivateAuthorizations: cliCtx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), } if replacesCertID != "" { request.ReplacesCertID = replacesCertID } - certRes, err := client.Certificate.Obtain(cliCtx.Context, request) + certRes, err := client.Certificate.Obtain(ctx, request) if err != nil { log.Fatal("Could not obtain the certificate.", "error", err) } @@ -275,13 +280,13 @@ func renewForDomains(cliCtx *cli.Context, account *Account, keyType certcrypto.K addPathToMetadata(meta, domain, certRes, certsStorage) - return launchHook(cliCtx.Context, cliCtx.String(flgRenewHook), cliCtx.Duration(flgRenewHookTimeout), meta) + return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta) } -func renewForCSR(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { - csr, err := readCSRFile(cliCtx.String(flgCSR)) +func renewForCSR(ctx context.Context, cmd *cli.Command, account *Account, keyType certcrypto.KeyType, certsStorage *CertificatesStorage, bundle bool, meta map[string]string) error { + csr, err := readCSRFile(cmd.String(flgCSR)) if err != nil { - log.Fatal("Could not read CSR file.", "flag", flgCSR, "filepath", cliCtx.String(flgCSR), "error", err) + log.Fatal("Could not read CSR file.", "flag", flgCSR, "filepath", cmd.String(flgCSR), "error", err) } domain, err := certcrypto.GetCSRMainDomain(csr) @@ -306,10 +311,10 @@ func renewForCSR(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyTy var client *lego.Client - if !cliCtx.Bool(flgARIDisable) { - client = setupClient(cliCtx, account, keyType) + if !cmd.Bool(flgARIDisable) { + client = setupClient(cmd, account, keyType) - ariRenewalTime = getARIRenewalTime(cliCtx, cert, domain, client) + ariRenewalTime = getARIRenewalTime(ctx, cmd, cert, domain, client) if ariRenewalTime != nil { now := time.Now().UTC() @@ -326,12 +331,12 @@ func renewForCSR(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyTy } } - if ariRenewalTime == nil && !needRenewal(cert, domain, cliCtx.Int(flgRenewDays), cliCtx.Bool(flgRenewDynamic)) { + if ariRenewalTime == nil && !needRenewal(cert, domain, cmd.Int(flgRenewDays), cmd.Bool(flgRenewDynamic)) { return nil } if client == nil { - client = setupClient(cliCtx, account, keyType) + client = setupClient(cmd, account, keyType) } // This is just meant to be informal for the user. @@ -340,19 +345,19 @@ func renewForCSR(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyTy request := certificate.ObtainForCSRRequest{ CSR: csr, - NotBefore: getTime(cliCtx, flgNotBefore), - NotAfter: getTime(cliCtx, flgNotAfter), + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), Bundle: bundle, - PreferredChain: cliCtx.String(flgPreferredChain), - Profile: cliCtx.String(flgProfile), - AlwaysDeactivateAuthorizations: cliCtx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), } if replacesCertID != "" { request.ReplacesCertID = replacesCertID } - certRes, err := client.Certificate.ObtainForCSR(cliCtx.Context, request) + certRes, err := client.Certificate.ObtainForCSR(ctx, request) if err != nil { log.Fatal("Could not obtain the certificate fro CSR.", "error", err) } @@ -361,7 +366,7 @@ func renewForCSR(cliCtx *cli.Context, account *Account, keyType certcrypto.KeyTy addPathToMetadata(meta, domain, certRes, certsStorage) - return launchHook(cliCtx.Context, cliCtx.String(flgRenewHook), cliCtx.Duration(flgRenewHookTimeout), meta) + return launchHook(ctx, cmd.String(flgRenewHook), cmd.Duration(flgRenewHookTimeout), meta) } func needRenewal(x509Cert *x509.Certificate, domain string, days int, dynamic bool) bool { @@ -409,12 +414,12 @@ func needRenewalDynamic(x509Cert *x509.Certificate, domain string, now time.Time } // getARIRenewalTime checks if the certificate needs to be renewed using the renewalInfo endpoint. -func getARIRenewalTime(cliCtx *cli.Context, cert *x509.Certificate, domain string, client *lego.Client) *time.Time { +func getARIRenewalTime(ctx context.Context, cmd *cli.Command, cert *x509.Certificate, domain string, client *lego.Client) *time.Time { if cert.IsCA { log.Fatal("Certificate bundle starts with a CA certificate.", "domain", domain) } - renewalInfo, err := client.Certificate.GetRenewalInfo(cliCtx.Context, certificate.RenewalInfoRequest{Cert: cert}) + renewalInfo, err := client.Certificate.GetRenewalInfo(ctx, certificate.RenewalInfoRequest{Cert: cert}) if err != nil { if errors.Is(err, api.ErrNoARI) { log.Warn("acme: the server does not advertise a renewal info endpoint.", "domain", domain, "errorr", err) @@ -428,7 +433,7 @@ func getARIRenewalTime(cliCtx *cli.Context, cert *x509.Certificate, domain strin now := time.Now().UTC() - renewalTime := renewalInfo.ShouldRenewAt(now, cliCtx.Duration(flgARIWaitToRenewDuration)) + renewalTime := renewalInfo.ShouldRenewAt(now, cmd.Duration(flgARIWaitToRenewDuration)) if renewalTime == nil { log.Info("acme: renewalInfo endpoint indicates that renewal is not needed.", "domain", domain) return nil diff --git a/cmd/cmd_revoke.go b/cmd/cmd_revoke.go index 9a48c13f9..9d458ff63 100644 --- a/cmd/cmd_revoke.go +++ b/cmd/cmd_revoke.go @@ -1,9 +1,11 @@ package cmd import ( + "context" + "github.com/go-acme/lego/v5/acme" "github.com/go-acme/lego/v5/log" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // Flag names. @@ -37,19 +39,19 @@ func createRevoke() *cli.Command { } } -func revoke(cliCtx *cli.Context) error { - account, keyType := setupAccount(cliCtx, NewAccountsStorage(cliCtx)) +func revoke(ctx context.Context, cmd *cli.Command) error { + account, keyType := setupAccount(ctx, cmd, NewAccountsStorage(cmd)) if account.Registration == nil { log.Fatal("Account is not registered. Use 'run' to register a new account.", "email", account.Email) } - client := newClient(cliCtx, account, keyType) + client := newClient(cmd, account, keyType) - certsStorage := NewCertificatesStorage(cliCtx) + certsStorage := NewCertificatesStorage(cmd) certsStorage.CreateRootFolder() - for _, domain := range cliCtx.StringSlice(flgDomains) { + for _, domain := range cmd.StringSlice(flgDomains) { log.Info("Trying to revoke the certificate.", "domain", domain) certBytes, err := certsStorage.ReadFile(domain, certExt) @@ -57,16 +59,16 @@ func revoke(cliCtx *cli.Context) error { log.Fatal("Error while revoking the certificate.", "domain", domain, "error", err) } - reason := cliCtx.Uint(flgReason) + reason := cmd.Uint(flgReason) - err = client.Certificate.RevokeWithReason(cliCtx.Context, certBytes, &reason) + err = client.Certificate.RevokeWithReason(ctx, certBytes, &reason) if err != nil { log.Fatal("Error while revoking the certificate.", "domain", domain, "error", err) } log.Info("Certificate was revoked.", "domain", domain) - if cliCtx.Bool(flgKeep) { + if cmd.Bool(flgKeep) { return nil } diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 1c42213c3..7653d8164 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -2,6 +2,7 @@ package cmd import ( "bufio" + "context" "fmt" "os" "strings" @@ -11,7 +12,7 @@ import ( "github.com/go-acme/lego/v5/lego" "github.com/go-acme/lego/v5/log" "github.com/go-acme/lego/v5/registration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // Flag names. @@ -32,11 +33,11 @@ func createRun() *cli.Command { return &cli.Command{ Name: "run", Usage: "Register an account, then create and install a certificate", - Before: func(ctx *cli.Context) error { + Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { // we require either domains or csr, but not both - hasDomains := len(ctx.StringSlice(flgDomains)) > 0 + hasDomains := len(cmd.StringSlice(flgDomains)) > 0 - hasCsr := ctx.String(flgCSR) != "" + hasCsr := cmd.String(flgCSR) != "" if hasDomains && hasCsr { log.Fatal("Please specify either --domains/-d or --csr/-c, but not both") } @@ -45,7 +46,7 @@ func createRun() *cli.Command { log.Fatal("Please specify --domains/-d (or --csr/-c if you already have a CSR)") } - return nil + return ctx, nil }, Action: run, Flags: []cli.Flag{ @@ -59,14 +60,18 @@ func createRun() *cli.Command { " Only works if the CSR is generated by lego.", }, &cli.TimestampFlag{ - Name: flgNotBefore, - Usage: "Set the notBefore field in the certificate (RFC3339 format)", - Layout: time.RFC3339, + Name: flgNotBefore, + Usage: "Set the notBefore field in the certificate (RFC3339 format)", + Config: cli.TimestampConfig{ + Layouts: []string{time.RFC3339}, + }, }, &cli.TimestampFlag{ - Name: flgNotAfter, - Usage: "Set the notAfter field in the certificate (RFC3339 format)", - Layout: time.RFC3339, + Name: flgNotAfter, + Usage: "Set the notAfter field in the certificate (RFC3339 format)", + Config: cli.TimestampConfig{ + Layouts: []string{time.RFC3339}, + }, }, &cli.StringFlag{ Name: flgPrivateKey, @@ -109,15 +114,15 @@ generated by lego and certificates obtained from the ACME server. Making regular backups of this folder is ideal. ` -func run(cliCtx *cli.Context) error { - accountsStorage := NewAccountsStorage(cliCtx) +func run(ctx context.Context, cmd *cli.Command) error { + accountsStorage := NewAccountsStorage(cmd) - account, keyType := setupAccount(cliCtx, accountsStorage) + account, keyType := setupAccount(ctx, cmd, accountsStorage) - client := setupClient(cliCtx, account, keyType) + client := setupClient(cmd, account, keyType) if account.Registration == nil { - reg, err := register(cliCtx, client) + reg, err := register(ctx, cmd, client) if err != nil { log.Fatal("Could not complete registration.", "error", err) } @@ -130,10 +135,10 @@ func run(cliCtx *cli.Context) error { fmt.Printf(rootPathWarningMessage, accountsStorage.GetRootPath()) } - certsStorage := NewCertificatesStorage(cliCtx) + certsStorage := NewCertificatesStorage(cmd) certsStorage.CreateRootFolder() - cert, err := obtainCertificate(cliCtx, client) + cert, err := obtainCertificate(ctx, cmd, client) 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. @@ -148,12 +153,12 @@ func run(cliCtx *cli.Context) error { addPathToMetadata(meta, cert.Domain, cert, certsStorage) - return launchHook(cliCtx.Context, cliCtx.String(flgRunHook), cliCtx.Duration(flgRunHookTimeout), meta) + return launchHook(ctx, cmd.String(flgRunHook), cmd.Duration(flgRunHookTimeout), meta) } -func handleTOS(ctx *cli.Context, client *lego.Client) bool { +func handleTOS(cmd *cli.Command, client *lego.Client) bool { // Check for a global accept override - if ctx.Bool(flgAcceptTOS) { + if cmd.Bool(flgAcceptTOS) { return true } @@ -181,61 +186,61 @@ func handleTOS(ctx *cli.Context, client *lego.Client) bool { } } -func register(cliCtx *cli.Context, client *lego.Client) (*registration.Resource, error) { - accepted := handleTOS(cliCtx, client) +func register(ctx context.Context, cmd *cli.Command, client *lego.Client) (*registration.Resource, error) { + accepted := handleTOS(cmd, client) if !accepted { log.Fatal("You did not accept the TOS. Unable to proceed.") } - if cliCtx.Bool(flgEAB) { - kid := cliCtx.String(flgKID) - hmacEncoded := cliCtx.String(flgHMAC) + if cmd.Bool(flgEAB) { + kid := cmd.String(flgKID) + hmacEncoded := cmd.String(flgHMAC) if kid == "" || hmacEncoded == "" { log.Fatal(fmt.Sprintf("Requires arguments --%s and --%s.", flgKID, flgHMAC)) } - return client.Registration.RegisterWithExternalAccountBinding(cliCtx.Context, registration.RegisterEABOptions{ + return client.Registration.RegisterWithExternalAccountBinding(ctx, registration.RegisterEABOptions{ TermsOfServiceAgreed: accepted, Kid: kid, HmacEncoded: hmacEncoded, }) } - return client.Registration.Register(cliCtx.Context, registration.RegisterOptions{TermsOfServiceAgreed: true}) + return client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true}) } -func obtainCertificate(cliCtx *cli.Context, client *lego.Client) (*certificate.Resource, error) { - bundle := !cliCtx.Bool(flgNoBundle) +func obtainCertificate(ctx context.Context, cmd *cli.Command, client *lego.Client) (*certificate.Resource, error) { + bundle := !cmd.Bool(flgNoBundle) - domains := cliCtx.StringSlice(flgDomains) + domains := cmd.StringSlice(flgDomains) if len(domains) > 0 { // obtain a certificate, generating a new private key request := certificate.ObtainRequest{ Domains: domains, - MustStaple: cliCtx.Bool(flgMustStaple), - NotBefore: getTime(cliCtx, flgNotBefore), - NotAfter: getTime(cliCtx, flgNotAfter), + MustStaple: cmd.Bool(flgMustStaple), + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), Bundle: bundle, - PreferredChain: cliCtx.String(flgPreferredChain), - Profile: cliCtx.String(flgProfile), - AlwaysDeactivateAuthorizations: cliCtx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), } - if cliCtx.IsSet(flgPrivateKey) { + if cmd.IsSet(flgPrivateKey) { var err error - request.PrivateKey, err = loadPrivateKey(cliCtx.String(flgPrivateKey)) + request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey)) if err != nil { return nil, fmt.Errorf("load private key: %w", err) } } - return client.Certificate.Obtain(cliCtx.Context, request) + return client.Certificate.Obtain(ctx, request) } // read the CSR - csr, err := readCSRFile(cliCtx.String(flgCSR)) + csr, err := readCSRFile(cmd.String(flgCSR)) if err != nil { return nil, err } @@ -243,22 +248,22 @@ func obtainCertificate(cliCtx *cli.Context, client *lego.Client) (*certificate.R // obtain a certificate for this CSR request := certificate.ObtainForCSRRequest{ CSR: csr, - NotBefore: getTime(cliCtx, flgNotBefore), - NotAfter: getTime(cliCtx, flgNotAfter), + NotBefore: cmd.Timestamp(flgNotBefore), + NotAfter: cmd.Timestamp(flgNotAfter), Bundle: bundle, - PreferredChain: cliCtx.String(flgPreferredChain), - Profile: cliCtx.String(flgProfile), - AlwaysDeactivateAuthorizations: cliCtx.Bool(flgAlwaysDeactivateAuthorizations), + PreferredChain: cmd.String(flgPreferredChain), + Profile: cmd.String(flgProfile), + AlwaysDeactivateAuthorizations: cmd.Bool(flgAlwaysDeactivateAuthorizations), } - if cliCtx.IsSet(flgPrivateKey) { + if cmd.IsSet(flgPrivateKey) { var err error - request.PrivateKey, err = loadPrivateKey(cliCtx.String(flgPrivateKey)) + request.PrivateKey, err = loadPrivateKey(cmd.String(flgPrivateKey)) if err != nil { return nil, fmt.Errorf("load private key: %w", err) } } - return client.Certificate.ObtainForCSR(cliCtx.Context, request) + return client.Certificate.ObtainForCSR(ctx, request) } diff --git a/cmd/flags.go b/cmd/flags.go index 317ef5cfb..77ca4faef 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -2,11 +2,10 @@ package cmd import ( "fmt" - "time" "github.com/go-acme/lego/v5/certificate" "github.com/go-acme/lego/v5/lego" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "software.sslmate.com/src/go-pkcs12" ) @@ -74,7 +73,7 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.StringFlag{ Name: flgServer, Aliases: []string{"s"}, - EnvVars: []string{envServer}, + Sources: cli.EnvVars(envServer), Usage: "CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client.", Value: lego.LEDirectoryProduction, }, @@ -86,7 +85,7 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.StringFlag{ Name: flgEmail, Aliases: []string{"m"}, - EnvVars: []string{envEmail}, + Sources: cli.EnvVars(envEmail), Usage: "Email used for registration and recovery contact.", }, &cli.BoolFlag{ @@ -100,17 +99,17 @@ func CreateFlags(defaultPath string) []cli.Flag { }, &cli.BoolFlag{ Name: flgEAB, - EnvVars: []string{envEAB}, + Sources: cli.EnvVars(envEAB), Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.", }, &cli.StringFlag{ Name: flgKID, - EnvVars: []string{envEABKID}, + Sources: cli.EnvVars(envEABKID), Usage: "Key identifier from External CA. Used for External Account Binding.", }, &cli.StringFlag{ Name: flgHMAC, - EnvVars: []string{envEABHMAC}, + Sources: cli.EnvVars(envEABHMAC), Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.", }, &cli.StringFlag{ @@ -125,7 +124,7 @@ func CreateFlags(defaultPath string) []cli.Flag { }, &cli.StringFlag{ Name: flgPath, - EnvVars: []string{envPath}, + Sources: cli.EnvVars(envPath), Usage: "Directory to use for storing the data.", Value: defaultPath, }, @@ -222,19 +221,19 @@ func CreateFlags(defaultPath string) []cli.Flag { &cli.BoolFlag{ Name: flgPFX, Usage: "Generate an additional .pfx (PKCS#12) file by concatenating the .key and .crt and issuer .crt files together.", - EnvVars: []string{envPFX}, + Sources: cli.EnvVars(envPFX), }, &cli.StringFlag{ Name: flgPFXPass, Usage: "The password used to encrypt the .pfx (PCKS#12) file.", Value: pkcs12.DefaultPassword, - EnvVars: []string{envPFXPassword}, + Sources: cli.EnvVars(envPFXPassword), }, &cli.StringFlag{ Name: flgPFXFormat, Usage: "The encoding format to use when encrypting the .pfx (PCKS#12) file. Supported: RC2, DES, SHA256.", Value: "RC2", - EnvVars: []string{envPFXFormat}, + Sources: cli.EnvVars(envPFXFormat), }, &cli.IntFlag{ Name: flgCertTimeout, @@ -252,12 +251,3 @@ func CreateFlags(defaultPath string) []cli.Flag { }, } } - -func getTime(ctx *cli.Context, name string) time.Time { - value := ctx.Timestamp(name) - if value == nil { - return time.Time{} - } - - return *value -} diff --git a/cmd/lego/main.go b/cmd/lego/main.go index 1d03d3896..b5e719706 100644 --- a/cmd/lego/main.go +++ b/cmd/lego/main.go @@ -3,6 +3,7 @@ package main import ( + "context" "fmt" "os" "path/filepath" @@ -10,21 +11,10 @@ import ( "github.com/go-acme/lego/v5/cmd" "github.com/go-acme/lego/v5/log" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func main() { - app := cli.NewApp() - app.Name = "lego" - app.HelpName = "lego" - app.Usage = "Let's Encrypt client written in Go" - app.EnableBashCompletion = true - - app.Version = getVersion() - cli.VersionPrinter = func(c *cli.Context) { - fmt.Printf("lego version %s %s/%s\n", c.App.Version, runtime.GOOS, runtime.GOARCH) - } - var defaultPath string cwd, err := os.Getwd() @@ -32,13 +22,25 @@ func main() { defaultPath = filepath.Join(cwd, ".lego") } - app.Flags = cmd.CreateFlags(defaultPath) + app := &cli.Command{ + Name: "lego", + Usage: "Let's Encrypt client written in Go", + Version: getVersion(), + EnableShellCompletion: true, + Flags: cmd.CreateFlags(defaultPath), + Before: cmd.Before, + Commands: cmd.CreateCommands(), + } - app.Before = cmd.Before + cli.VersionPrinter = func(cmd *cli.Command) { + fmt.Printf("lego version %s %s/%s\n", cmd.Version, runtime.GOOS, runtime.GOARCH) + } app.Commands = cmd.CreateCommands() - err = app.Run(os.Args) + ctx := context.Background() + + err = app.Run(ctx, os.Args) if err != nil { log.Fatal("Error", "error", err) } diff --git a/cmd/setup.go b/cmd/setup.go index 5d84e8b4b..bec0f2702 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -18,27 +18,27 @@ import ( "github.com/go-acme/lego/v5/log" "github.com/go-acme/lego/v5/registration" "github.com/hashicorp/go-retryablehttp" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const filePerm os.FileMode = 0o600 // setupClient creates a new client with challenge settings. -func setupClient(ctx *cli.Context, account *Account, keyType certcrypto.KeyType) *lego.Client { - client := newClient(ctx, account, keyType) +func setupClient(cmd *cli.Command, account *Account, keyType certcrypto.KeyType) *lego.Client { + client := newClient(cmd, account, keyType) - setupChallenges(ctx, client) + setupChallenges(cmd, client) return client } -func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) { - keyType := getKeyType(ctx) +func setupAccount(ctx context.Context, cmd *cli.Command, accountsStorage *AccountsStorage) (*Account, certcrypto.KeyType) { + keyType := getKeyType(cmd) privateKey := accountsStorage.GetPrivateKey(keyType) var account *Account if accountsStorage.ExistsAccountFilePath() { - account = accountsStorage.LoadAccount(privateKey) + account = accountsStorage.LoadAccount(ctx, privateKey) } else { account = &Account{Email: accountsStorage.GetEmail(), key: privateKey} } @@ -46,23 +46,23 @@ func setupAccount(ctx *cli.Context, accountsStorage *AccountsStorage) (*Account, return account, keyType } -func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyType) *lego.Client { +func newClient(cmd *cli.Command, acc registration.User, keyType certcrypto.KeyType) *lego.Client { config := lego.NewConfig(acc) - config.CADirURL = ctx.String(flgServer) + config.CADirURL = cmd.String(flgServer) config.Certificate = lego.CertificateConfig{ KeyType: keyType, - Timeout: time.Duration(ctx.Int(flgCertTimeout)) * time.Second, - OverallRequestLimit: ctx.Int(flgOverallRequestLimit), - EnableCommonName: !ctx.Bool(flgDisableCommonName), + Timeout: time.Duration(cmd.Int(flgCertTimeout)) * time.Second, + OverallRequestLimit: cmd.Int(flgOverallRequestLimit), + EnableCommonName: !cmd.Bool(flgDisableCommonName), } - config.UserAgent = getUserAgent(ctx) + config.UserAgent = getUserAgent(cmd) - if ctx.IsSet(flgHTTPTimeout) { - config.HTTPClient.Timeout = time.Duration(ctx.Int(flgHTTPTimeout)) * time.Second + if cmd.IsSet(flgHTTPTimeout) { + config.HTTPClient.Timeout = time.Duration(cmd.Int(flgHTTPTimeout)) * time.Second } - if ctx.Bool(flgTLSSkipVerify) { + if cmd.Bool(flgTLSSkipVerify) { defaultTransport, ok := config.HTTPClient.Transport.(*http.Transport) if ok { // This is always true because the default client used by the CLI defined the transport. tr := defaultTransport.Clone() @@ -88,7 +88,7 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy log.Fatal("Could not create client.", "error", err) } - if client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) { + if client.GetExternalAccountRequired() && !cmd.IsSet(flgEAB) { log.Fatal(fmt.Sprintf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC)) } @@ -96,8 +96,8 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy } // getKeyType the type from which private keys should be generated. -func getKeyType(ctx *cli.Context) certcrypto.KeyType { - keyType := ctx.String(flgKeyType) +func getKeyType(cmd *cli.Command) certcrypto.KeyType { + keyType := cmd.String(flgKeyType) switch strings.ToUpper(keyType) { case "RSA2048": return certcrypto.RSA2048 @@ -118,8 +118,8 @@ func getKeyType(ctx *cli.Context) certcrypto.KeyType { return "" } -func getUserAgent(ctx *cli.Context) string { - return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", ctx.String(flgUserAgent), ctx.App.Version)) +func getUserAgent(cmd *cli.Command) string { + return strings.TrimSpace(fmt.Sprintf("%s lego-cli/%s", cmd.String(flgUserAgent), cmd.Version)) } func createNonExistingFolder(path string) error { diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index 19e574ed9..77b1a6e75 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -16,30 +16,30 @@ import ( "github.com/go-acme/lego/v5/providers/http/memcached" "github.com/go-acme/lego/v5/providers/http/s3" "github.com/go-acme/lego/v5/providers/http/webroot" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -func setupChallenges(ctx *cli.Context, client *lego.Client) { - if !ctx.Bool(flgHTTP) && !ctx.Bool(flgTLS) && !ctx.IsSet(flgDNS) { +func setupChallenges(cmd *cli.Command, client *lego.Client) { + if !cmd.Bool(flgHTTP) && !cmd.Bool(flgTLS) && !cmd.IsSet(flgDNS) { log.Fatal(fmt.Sprintf("No challenge selected. You must specify at least one challenge: `--%s`, `--%s`, `--%s`.", flgHTTP, flgTLS, flgDNS)) } - if ctx.Bool(flgHTTP) { - err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(ctx), http01.SetDelay(ctx.Duration(flgHTTPDelay))) + if cmd.Bool(flgHTTP) { + err := client.Challenge.SetHTTP01Provider(setupHTTPProvider(cmd), http01.SetDelay(cmd.Duration(flgHTTPDelay))) if err != nil { log.Fatal("Could not set HTTP challenge provider.", "error", err) } } - if ctx.Bool(flgTLS) { - err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(ctx), tlsalpn01.SetDelay(ctx.Duration(flgTLSDelay))) + if cmd.Bool(flgTLS) { + err := client.Challenge.SetTLSALPN01Provider(setupTLSProvider(cmd), tlsalpn01.SetDelay(cmd.Duration(flgTLSDelay))) if err != nil { log.Fatal("Could not set TLS challenge provider.", "error", err) } } - if ctx.IsSet(flgDNS) { - err := setupDNS(ctx, client) + if cmd.IsSet(flgDNS) { + err := setupDNS(cmd, client) if err != nil { log.Fatal("Could not set DNS challenge provider.", "error", err) } @@ -47,42 +47,42 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) { } //nolint:gocyclo // the complexity is expected. -func setupHTTPProvider(ctx *cli.Context) challenge.Provider { +func setupHTTPProvider(cmd *cli.Command) challenge.Provider { switch { - case ctx.IsSet(flgHTTPWebroot): - ps, err := webroot.NewHTTPProvider(ctx.String(flgHTTPWebroot)) + case cmd.IsSet(flgHTTPWebroot): + ps, err := webroot.NewHTTPProvider(cmd.String(flgHTTPWebroot)) if err != nil { log.Fatal("Could not create the webroot provider.", - "flag", flgHTTPWebroot, "webRoot", ctx.String(flgHTTPWebroot), "error", err) + "flag", flgHTTPWebroot, "webRoot", cmd.String(flgHTTPWebroot), "error", err) } return ps - case ctx.IsSet(flgHTTPMemcachedHost): - ps, err := memcached.NewMemcachedProvider(ctx.StringSlice(flgHTTPMemcachedHost)) + case cmd.IsSet(flgHTTPMemcachedHost): + ps, err := memcached.NewMemcachedProvider(cmd.StringSlice(flgHTTPMemcachedHost)) if err != nil { log.Fatal("Could not create the memcached provider.", - "flag", flgHTTPMemcachedHost, "memcachedHosts", strings.Join(ctx.StringSlice(flgHTTPMemcachedHost), ", "), "error", err) + "flag", flgHTTPMemcachedHost, "memcachedHosts", strings.Join(cmd.StringSlice(flgHTTPMemcachedHost), ", "), "error", err) } return ps - case ctx.IsSet(flgHTTPS3Bucket): - ps, err := s3.NewHTTPProvider(ctx.String(flgHTTPS3Bucket)) + case cmd.IsSet(flgHTTPS3Bucket): + ps, err := s3.NewHTTPProvider(cmd.String(flgHTTPS3Bucket)) if err != nil { log.Fatal("Could not create the S3 provider.", - "flag", flgHTTPS3Bucket, "bucket", ctx.String(flgHTTPS3Bucket), "error", err) + "flag", flgHTTPS3Bucket, "bucket", cmd.String(flgHTTPS3Bucket), "error", err) } return ps - case ctx.IsSet(flgHTTPPort): - iface := ctx.String(flgHTTPPort) + case cmd.IsSet(flgHTTPPort): + iface := cmd.String(flgHTTPPort) if !strings.Contains(iface, ":") { log.Fatal( fmt.Sprintf("The --%s switch only accepts interface:port or :port for its argument.", flgHTTPPort), - "flag", flgHTTPPort, "port", ctx.String(flgHTTPPort), + "flag", flgHTTPPort, "port", cmd.String(flgHTTPPort), ) } @@ -97,20 +97,20 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { Address: net.JoinHostPort(host, port), }) - if header := ctx.String(flgHTTPProxyHeader); header != "" { + if header := cmd.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } return srv - case ctx.Bool(flgHTTP): + case cmd.Bool(flgHTTP): srv := http01.NewProviderServerWithOptions(http01.Options{ // TODO(ldez): set network stack Network: "tcp", Address: net.JoinHostPort("", ":80"), }) - if header := ctx.String(flgHTTPProxyHeader); header != "" { + if header := cmd.String(flgHTTPProxyHeader); header != "" { srv.SetProxyHeader(header) } @@ -122,10 +122,10 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { } } -func setupTLSProvider(ctx *cli.Context) challenge.Provider { +func setupTLSProvider(cmd *cli.Command) challenge.Provider { switch { - case ctx.IsSet(flgTLSPort): - iface := ctx.String(flgTLSPort) + case cmd.IsSet(flgTLSPort): + iface := cmd.String(flgTLSPort) if !strings.Contains(iface, ":") { log.Fatal(fmt.Sprintf("The --%s switch only accepts interface:port or :port for its argument.", flgTLSPort)) } @@ -142,7 +142,7 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider { Port: port, }) - case ctx.Bool(flgTLS): + case cmd.Bool(flgTLS): return tlsalpn01.NewProviderServerWithOptions(tlsalpn01.Options{ // TODO(ldez): set network stack Network: "tcp", @@ -154,62 +154,62 @@ func setupTLSProvider(ctx *cli.Context) challenge.Provider { } } -func setupDNS(ctx *cli.Context, client *lego.Client) error { - err := checkPropagationExclusiveOptions(ctx) +func setupDNS(cmd *cli.Command, client *lego.Client) error { + err := checkPropagationExclusiveOptions(cmd) if err != nil { return err } - wait := ctx.Duration(flgDNSPropagationWait) + wait := cmd.Duration(flgDNSPropagationWait) if wait < 0 { return fmt.Errorf("'%s' cannot be negative", flgDNSPropagationWait) } - provider, err := dns.NewDNSChallengeProviderByName(ctx.String(flgDNS)) + provider, err := dns.NewDNSChallengeProviderByName(cmd.String(flgDNS)) if err != nil { return err } - opts := &dns01.Options{RecursiveNameservers: ctx.StringSlice(flgDNSResolvers)} + opts := &dns01.Options{RecursiveNameservers: cmd.StringSlice(flgDNSResolvers)} - if ctx.IsSet(flgDNSTimeout) { - opts.Timeout = time.Duration(ctx.Int(flgDNSTimeout)) * time.Second + if cmd.IsSet(flgDNSTimeout) { + opts.Timeout = time.Duration(cmd.Int(flgDNSTimeout)) * time.Second } dns01.SetDefaultClient(dns01.NewClient(opts)) err = client.Challenge.SetDNS01Provider(provider, - dns01.CondOption(ctx.Bool(flgDNSDisableCP) || ctx.Bool(flgDNSPropagationDisableANS), + dns01.CondOption(cmd.Bool(flgDNSDisableCP) || cmd.Bool(flgDNSPropagationDisableANS), dns01.DisableAuthoritativeNssPropagationRequirement()), - dns01.CondOption(ctx.Duration(flgDNSPropagationWait) > 0, + dns01.CondOption(cmd.Duration(flgDNSPropagationWait) > 0, // TODO(ldez): inside the next major version we will use flgDNSDisableCP here. // This will change the meaning of this flag to really disable all propagation checks. dns01.PropagationWait(wait, true)), - dns01.CondOption(ctx.Bool(flgDNSPropagationRNS), + dns01.CondOption(cmd.Bool(flgDNSPropagationRNS), dns01.RecursiveNSsPropagationRequirement()), ) return err } -func checkPropagationExclusiveOptions(ctx *cli.Context) error { - if ctx.IsSet(flgDNSDisableCP) { +func checkPropagationExclusiveOptions(cmd *cli.Command) error { + if cmd.IsSet(flgDNSDisableCP) { log.Warnf(log.LazySprintf("The flag '%s' is deprecated use '%s' instead.", flgDNSDisableCP, flgDNSPropagationDisableANS)) } - if (isSetBool(ctx, flgDNSDisableCP) || isSetBool(ctx, flgDNSPropagationDisableANS)) && ctx.IsSet(flgDNSPropagationWait) { + if (isSetBool(cmd, flgDNSDisableCP) || isSetBool(cmd, flgDNSPropagationDisableANS)) && cmd.IsSet(flgDNSPropagationWait) { return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationDisableANS, flgDNSPropagationWait) } - if isSetBool(ctx, flgDNSPropagationRNS) && ctx.IsSet(flgDNSPropagationWait) { + if isSetBool(cmd, flgDNSPropagationRNS) && cmd.IsSet(flgDNSPropagationWait) { return fmt.Errorf("'%s' and '%s' are mutually exclusive", flgDNSPropagationRNS, flgDNSPropagationWait) } return nil } -func isSetBool(ctx *cli.Context, name string) bool { - return ctx.IsSet(name) && ctx.Bool(name) +func isSetBool(cmd *cli.Command, name string) bool { + return cmd.IsSet(name) && cmd.Bool(name) } diff --git a/go.mod b/go.mod index 688a3e49f..3b2435925 100644 --- a/go.mod +++ b/go.mod @@ -84,7 +84,7 @@ require ( github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.28 github.com/transip/gotransip/v6 v6.26.1 github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 - github.com/urfave/cli/v2 v2.27.7 + github.com/urfave/cli/v3 v3.6.2 github.com/vinyldns/go-vinyldns v0.9.17 github.com/volcengine/volc-sdk-golang v1.0.233 github.com/vultr/govultr/v3 v3.26.1 @@ -136,7 +136,6 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/color v1.16.0 // indirect @@ -183,7 +182,6 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sacloud/go-http v0.1.9 // indirect github.com/sacloud/packages-go v0.0.12 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -200,7 +198,6 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect diff --git a/go.sum b/go.sum index 889e6b5b5..9f758e19c 100644 --- a/go.sum +++ b/go.sum @@ -259,8 +259,6 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -815,8 +813,6 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sacloud/api-client-go v0.3.3 h1:ZpSAyGpITA8UFO3Hq4qMHZLGuNI1FgxAxo4sqBnCKDs= github.com/sacloud/api-client-go v0.3.3/go.mod h1:0p3ukcWYXRCc2AUWTl1aA+3sXLvurvvDqhRaLZRLBwo= @@ -918,8 +914,8 @@ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqri github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 h1:/VaznPrb/b68e3iMvkr27fU7JqPKU4j7tIITZnjQX1k= github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419/go.mod h1:QN0/PdenvYWB0GRMz6JJbPeZz2Lph2iys1p8AFVHm2c= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= +github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/vinyldns/go-vinyldns v0.9.17 h1:hfPZfCaxcRBX6Gsgl42rLCeoal58/BH8kkvJShzjjdI= github.com/vinyldns/go-vinyldns v0.9.17/go.mod h1:pwWhE9K/leGDOIduVhRGvQ3ecVMHWRfEnKYUTEU3gB4= github.com/volcengine/volc-sdk-golang v1.0.233 h1:Hh2pzwu/Wq19rsZgNo3HdpjQB28D/F0+m6EjLVggmhM= @@ -932,8 +928,6 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yandex-cloud/go-genproto v0.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw= github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= github.com/yandex-cloud/go-sdk/services/dns v0.0.25 h1:BcGEuOnwq2X3LS2kvFC6BOdZkOq4Lc7XAYvzap/SJJY= diff --git a/internal/clihelp/generator.go b/internal/clihelp/generator.go index 8c1943e9a..40ac9d652 100644 --- a/internal/clihelp/generator.go +++ b/internal/clihelp/generator.go @@ -4,6 +4,7 @@ package main import ( "bytes" + "context" "fmt" "log" "os" @@ -11,7 +12,7 @@ import ( "text/template" "github.com/go-acme/lego/v5/cmd" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const outputFile = "../../docs/data/zz_cli_help.toml" @@ -35,7 +36,7 @@ type commandHelp struct { func main() { log.SetFlags(0) - err := generate() + err := generate(context.Background()) if err != nil { log.Fatal(err) } @@ -43,7 +44,7 @@ func main() { log.Println("cli_help.toml updated") } -func generate() error { +func generate(ctx context.Context) error { app := createStubApp() outputTpl := template.Must(template.New("output").Parse(baseTemplate)) @@ -59,7 +60,7 @@ func generate() error { {"lego", "help", "list"}, {"lego", "dnshelp"}, } { - content, err := run(app, args) + content, err := run(ctx, app, args) if err != nil { return fmt.Errorf("running %s failed: %w", args, err) } @@ -88,18 +89,16 @@ func generate() error { // - substitute "." for CWD in default config path, as the user will very likely see a different path // - do not include version information, because we're likely running against a snapshot // - skip DNS help and provider list, as initialization takes time, and we don't generate `lego dns --help` here. -func createStubApp() *cli.App { - app := cli.NewApp() - app.Name = "lego" - app.HelpName = "lego" - app.Usage = "Let's Encrypt client written in Go" - app.Flags = cmd.CreateFlags("./.lego") - app.Commands = cmd.CreateCommands() - - return app +func createStubApp() *cli.Command { + return &cli.Command{ + Name: "lego", + Usage: "Let's Encrypt client written in Go", + Flags: cmd.CreateFlags("./.lego"), + Commands: cmd.CreateCommands(), + } } -func run(app *cli.App, args []string) (h commandHelp, err error) { +func run(ctx context.Context, app *cli.Command, args []string) (h commandHelp, err error) { w := app.Writer defer func() { app.Writer = w }() @@ -108,7 +107,11 @@ func run(app *cli.App, args []string) (h commandHelp, err error) { app.Writer = &buf - if err := app.Run(args); err != nil { + if app.Command(args[1]) != nil { + app.Command(args[1]).Writer = app.Writer + } + + if err := app.Run(ctx, args); err != nil { return h, err } diff --git a/internal/releaser/releaser.go b/internal/releaser/releaser.go index 57b463933..57805cbe0 100644 --- a/internal/releaser/releaser.go +++ b/internal/releaser/releaser.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "go/ast" "go/parser" @@ -10,7 +11,7 @@ import ( "strconv" hcversion "github.com/hashicorp/go-version" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const flgMode = "mode" @@ -29,48 +30,48 @@ const ( ) func main() { - app := cli.NewApp() - app.Name = "lego-releaser" - app.Usage = "Lego releaser" - app.HelpName = "releaser" - app.Commands = []*cli.Command{ - { - Name: "release", - Usage: "Update file for a release", - Action: release, - Before: func(ctx *cli.Context) error { - mode := ctx.String("mode") - switch mode { - case modePatch, modeMinor, modeMajor: - return nil - default: - return fmt.Errorf("invalid mode: %s", mode) - } - }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: flgMode, - Aliases: []string{"m"}, - Value: modePatch, - Usage: fmt.Sprintf("The release mode: %s|%s|%s", modePatch, modeMinor, modeMajor), + app := cli.Command{ + Name: "lego-releaser", + Usage: "Lego releaser", + Commands: []*cli.Command{ + { + Name: "release", + Usage: "Update file for a release", + Action: release, + Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + mode := cmd.String("mode") + switch mode { + case modePatch, modeMinor, modeMajor: + return ctx, nil + default: + return ctx, fmt.Errorf("invalid mode: %s", mode) + } + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flgMode, + Aliases: []string{"m"}, + Value: modePatch, + Usage: fmt.Sprintf("The release mode: %s|%s|%s", modePatch, modeMinor, modeMajor), + }, }, }, - }, - { - Name: "detach", - Usage: "Update file post release", - Action: detach, + { + Name: "detach", + Usage: "Update file post release", + Action: detach, + }, }, } - err := app.Run(os.Args) + err := app.Run(context.Background(), os.Args) if err != nil { log.Fatal(err) } } -func release(ctx *cli.Context) error { - mode := ctx.String(flgMode) +func release(ctx context.Context, cmd *cli.Command) error { + mode := cmd.String(flgMode) currentVersion, err := readCurrentVersion(versionSourceFile) if err != nil { @@ -90,7 +91,7 @@ func release(ctx *cli.Context) error { return nil } -func detach(_ *cli.Context) error { +func detach(_ context.Context, _ *cli.Command) error { currentVersion, err := readCurrentVersion(versionSourceFile) if err != nil { return fmt.Errorf("read current version: %w", err)