feat: new list sub-commands

This commit is contained in:
Fernandez Ludovic 2026-01-29 18:09:52 +01:00
commit 1164cd4052
4 changed files with 272 additions and 146 deletions

View file

@ -1,137 +1,16 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
)
func createList() *cli.Command {
return &cli.Command{
Name: "list",
Usage: "Display certificates and accounts information.",
Action: list,
Flags: createListFlags(),
Name: "list",
Usage: "Display certificates and accounts information.",
Commands: []*cli.Command{
createListCertificates(),
createListAccounts(),
},
}
}
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, cmd)
}
func listCertificates(_ context.Context, cmd *cli.Command) error {
certsStorage := storage.NewCertificatesStorage(cmd.String(flgPath))
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.crt"))
if err != nil {
return err
}
names := cmd.Bool(flgNames)
if len(matches) == 0 {
if !names {
fmt.Println("No certificates found.")
}
return nil
}
if !names {
fmt.Println("Found the following certs:")
}
for _, filename := range matches {
if strings.HasSuffix(filename, storage.ExtIssuer) {
continue
}
data, err := os.ReadFile(filename)
if err != nil {
return err
}
pCert, err := certcrypto.ParsePEMCertificate(data)
if err != nil {
return err
}
name, err := certcrypto.GetCertificateMainDomain(pCert)
if err != nil {
return err
}
if names {
fmt.Println(name)
} else {
fmt.Println(" Certificate Name:", name)
fmt.Println(" Domains:", strings.Join(pCert.DNSNames, ", "))
fmt.Println(" Expiry Date:", pCert.NotAfter)
fmt.Println(" Certificate Path:", filename)
fmt.Println()
}
}
return nil
}
func listAccount(_ context.Context, cmd *cli.Command) error {
accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd))
if err != nil {
log.Fatal("Accounts storage initialization", log.ErrorAttr(err))
}
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
return err
}
if len(matches) == 0 {
fmt.Println("No accounts were found.")
return nil
}
fmt.Println("Found the following accounts:")
for _, filename := range matches {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
var account storage.Account
err = json.Unmarshal(data, &account)
if err != nil {
return err
}
uri, err := url.Parse(account.Registration.URI)
if err != nil {
return err
}
fmt.Println(" ID:", account.GetID())
fmt.Println(" Email:", account.Email)
fmt.Println(" Server:", uri.Host)
fmt.Println(" Path:", filepath.Dir(filename))
fmt.Println()
}
return nil
}

116
cmd/cmd_list_accounts.go Normal file
View file

@ -0,0 +1,116 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/go-acme/lego/v5/log"
"github.com/urfave/cli/v3"
)
type ListAccount struct {
storage.Account
Server string `json:"server,omitempty"`
Path string `json:"path,omitempty"`
}
func createListAccounts() *cli.Command {
return &cli.Command{
Name: "accounts",
Usage: "Display information about accounts.",
Action: listAccounts,
Flags: createListFlags(),
}
}
func listAccounts(ctx context.Context, cmd *cli.Command) error {
if cmd.Bool(flgFormatJSON) {
return listAccountsJSON(ctx, cmd)
}
return listAccountsText(ctx, cmd)
}
func listAccountsText(_ context.Context, cmd *cli.Command) error {
accounts, err := readAccounts(cmd)
if err != nil {
return err
}
if len(accounts) == 0 {
fmt.Println("No accounts were found.")
return nil
}
fmt.Println("Found the following accounts:")
for _, account := range accounts {
fmt.Println(account.GetID())
fmt.Println("├── Email:", account.Email)
fmt.Println("├── Server:", account.Server)
fmt.Println("└── Path:", account.Path)
fmt.Println()
}
return nil
}
func listAccountsJSON(_ context.Context, cmd *cli.Command) error {
accounts, err := readAccounts(cmd)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(accounts)
}
func readAccounts(cmd *cli.Command) ([]ListAccount, error) {
accountsStorage, err := storage.NewAccountsStorage(newAccountsStorageConfig(cmd))
if err != nil {
return nil, err
}
matches, err := filepath.Glob(filepath.Join(accountsStorage.GetRootPath(), "*", "*", "*.json"))
if err != nil {
return nil, err
}
var accounts []ListAccount
for _, filename := range matches {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var account storage.Account
err = json.Unmarshal(data, &account)
if err != nil {
return nil, err
}
var server string
uri, err := url.Parse(account.Registration.URI)
if err != nil {
log.Error("Parsing account registration URI.", log.ErrorAttr(err))
} else {
server = fmt.Sprintf("%s://%s", uri.Scheme, uri.Host)
}
accounts = append(accounts, ListAccount{
Account: account,
Server: server,
Path: filename,
})
}
return accounts, nil
}

View file

@ -0,0 +1,138 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-acme/lego/v5/certcrypto"
"github.com/go-acme/lego/v5/cmd/internal/storage"
"github.com/urfave/cli/v3"
)
type ListCertificate struct {
Name string `json:"name,omitempty"`
Domains []string `json:"domains,omitempty"`
IPs []string `json:"ips,omitempty"`
ExpirationDate string `json:"expirationDate,omitempty"`
Expired bool `json:"expired"`
Issuer string `json:"issuer,omitempty"`
Path string `json:"path,omitempty"`
}
func createListCertificates() *cli.Command {
return &cli.Command{
Name: "certificates",
Usage: "Display information about certificates.",
Action: listCertificates,
Flags: createListFlags(),
}
}
func listCertificates(ctx context.Context, cmd *cli.Command) error {
if cmd.Bool(flgFormatJSON) {
return listCertificatesJSON(ctx, cmd)
}
return listCertificatesText(ctx, cmd)
}
func listCertificatesText(_ context.Context, cmd *cli.Command) error {
certs, err := readCertificates(cmd)
if err != nil {
return err
}
if len(certs) == 0 {
fmt.Println("No certificates were found.")
return nil
}
fmt.Println("Found the following certificates:")
for _, info := range certs {
fmt.Println(info.Name)
if info.Expired {
fmt.Println("├── Status: this certificate is expired.")
}
if len(info.Domains) > 0 {
fmt.Println("├── Domains:", strings.Join(info.Domains, ", "))
}
if len(info.IPs) > 0 {
fmt.Println("├── IPs:", strings.Join(info.IPs, ","))
}
fmt.Println("├── Expiration Date:", info.ExpirationDate)
fmt.Println("├── Issuer:", info.Issuer)
fmt.Println("└── Certificate Path:", info.Path)
fmt.Println()
}
return nil
}
func listCertificatesJSON(_ context.Context, cmd *cli.Command) error {
certs, err := readCertificates(cmd)
if err != nil {
return err
}
return json.NewEncoder(os.Stdout).Encode(certs)
}
func readCertificates(cmd *cli.Command) ([]ListCertificate, error) {
certsStorage := storage.NewCertificatesStorage(cmd.String(flgPath))
matches, err := filepath.Glob(filepath.Join(certsStorage.GetRootPath(), "*.json"))
if err != nil {
return nil, err
}
var certificates []ListCertificate
for _, filename := range matches {
certFilename := strings.TrimSuffix(filename, storage.ExtResource) + storage.ExtCert
data, err := os.ReadFile(certFilename)
if err != nil {
return nil, err
}
pCert, err := certcrypto.ParsePEMCertificate(data)
if err != nil {
return nil, err
}
name := strings.TrimSuffix(filepath.Base(certFilename), storage.ExtCert)
certificates = append(certificates, ListCertificate{
Name: name,
Domains: pCert.DNSNames,
IPs: toStringSlice(pCert.IPAddresses),
ExpirationDate: pCert.NotAfter.String(),
Expired: pCert.NotAfter.Before(time.Now()),
Issuer: pCert.Issuer.String(),
Path: certFilename,
})
}
return certificates, nil
}
func toStringSlice[T fmt.Stringer](values []T) []string {
var s []string
for _, value := range values {
s = append(s, value.String())
}
return s
}

View file

@ -149,10 +149,9 @@ const (
flgReason = "reason"
)
// Flag names related to the list command.
// Flag names related to the list commands.
const (
flgAccounts = "accounts"
flgNames = "names"
flgFormatJSON = "json"
)
func toEnvName(flg string) string {
@ -671,22 +670,6 @@ func createRevokeFlags() []cli.Flag {
return flags
}
func createListFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: flgAccounts,
Aliases: []string{"a"},
Usage: "Display accounts.",
},
&cli.BoolFlag{
Name: flgNames,
Aliases: []string{"n"},
Usage: "Display certificate names only.",
},
createPathFlag(false),
}
}
func createRegisterFlags() []cli.Flag {
flags := []cli.Flag{
createPathFlag(true),
@ -699,6 +682,16 @@ func createRegisterFlags() []cli.Flag {
return flags
}
func createListFlags() []cli.Flag {
return []cli.Flag{
createPathFlag(false),
&cli.BoolFlag{
Name: flgFormatJSON,
Usage: "Format the output as JSON.",
},
}
}
func createAcceptFlag() cli.Flag {
return &cli.BoolFlag{
Name: flgAcceptTOS,