Files
uncloud/cmd/uc/caddy/cert/ls.go
T

168 lines
4.5 KiB
Go

package cert
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"
"time"
"charm.land/lipgloss/v2"
"github.com/caddyserver/certmagic"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
)
type listOptions struct {
machine string
output string
}
func NewListCommand() *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List certificates in cluster storage for Caddy.",
Long: `List certificates stored in the Uncloud cluster storage for Caddy.
Caddy must use the Uncloud storage module (https://github.com/unlabs-dev/caddy-uncloud)
configured with 'storage uncloud' in the global options for its certificates to appear here.
This inventory does not check whether Caddy currently serves or trusts a certificate.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine to read certificate storage from. (default is connected machine)")
cmd.Flags().StringVarP(&opts.output, "output", "o", "",
"Output format: 'json' or empty for a human-readable table.")
completion.MachinesFlag(cmd)
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
if opts.output != "" && opts.output != "json" {
return fmt.Errorf("unsupported output format '%s' (supported: json)", opts.output)
}
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
certs, listErr := clusterClient.Caddy.ListCertificates(ctx,
client.CaddyListCertificatesOptions{Machine: opts.machine})
if len(certs) > 0 || listErr == nil {
if err = printCertificates(os.Stdout, certs, opts.output); err != nil {
return fmt.Errorf("print Caddy certificates: %w", err)
}
}
if listErr != nil {
if len(certs) > 0 {
return fmt.Errorf("certificate list is partial: %w", listErr)
}
return fmt.Errorf("list Caddy certificates: %w", listErr)
}
return nil
}
func printCertificates(out io.Writer, certs []api.IssuedCertificate, output string) error {
type certificate struct {
api.IssuedCertificate
Fingerprint string
}
items := make([]certificate, 0, len(certs))
for _, cert := range certs {
fingerprint := cert.Fingerprint()
items = append(items, certificate{
IssuedCertificate: cert,
Fingerprint: hex.EncodeToString(fingerprint[:]),
})
}
slices.SortFunc(items, func(a, b certificate) int {
if cmp := strings.Compare(a.SAN, b.SAN); cmp != 0 {
return cmp
}
if cmp := a.Chain[0].NotAfter.Compare(b.Chain[0].NotAfter); cmp != 0 {
return cmp
}
return strings.Compare(a.Fingerprint, b.Fingerprint)
})
if output == "json" {
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")
if err := encoder.Encode(items); err != nil {
return fmt.Errorf("marshal certificates: %w", err)
}
return nil
}
if len(items) == 0 {
_, err := fmt.Fprintln(out, "No Caddy certificates found in cluster storage.")
return err
}
t := tui.NewTable()
t.Headers("ID", "NAME", "ISSUER", "EXPIRES")
for _, item := range items {
t.Row(
item.Fingerprint[:12],
item.SAN,
formatIssuer(item.IssuerData),
formatExpiry(item.Chain[0].NotAfter),
)
}
_, err := lipgloss.Fprintln(out, t)
return err
}
func formatIssuer(data api.CertificateIssuerData) string {
if data.ACME == nil {
return "unknown"
}
switch data.ACME.CA {
case certmagic.LetsEncryptProductionCA:
return "Let's Encrypt"
case certmagic.LetsEncryptStagingCA:
return "Let's Encrypt (staging)"
case certmagic.ZeroSSLProductionCA:
return "ZeroSSL"
case certmagic.GoogleTrustProductionCA:
return "Google Trust Services"
case certmagic.GoogleTrustStagingCA:
return "Google Trust Services (staging)"
default:
return data.ACME.CA
}
}
func formatExpiry(expires time.Time) string {
delta := expires.Sub(time.Now())
if delta <= 0 {
return fmt.Sprintf("%s (expired %s ago)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(-delta)))
}
return fmt.Sprintf("%s (%s)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(delta)))
}