1package cli
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "strings"
8 "time"
9
10 "github.com/spf13/cobra"
11
12 "github.com/bnema/gordon/internal/adapters/dto"
13)
14
15// tlsResolveControlPlane allows tests to override control-plane resolution.
16var tlsResolveControlPlane = resolveControlPlane
17
18func newTLSCmd() *cobra.Command {
19 var jsonOut bool
20
21 cmd := &cobra.Command{
22 Use: "tls",
23 Short: "Show public TLS certificate status",
24 RunE: func(cmd *cobra.Command, _ []string) error {
25 handle, err := tlsResolveControlPlane(cliConfigPath)
26 if err != nil {
27 return err
28 }
29 defer handle.close()
30 return runTLSStatusCmd(cmd.Context(), handle.plane, cmd.OutOrStdout(), jsonOut)
31 },
32 }
33
34 cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
35
36 return cmd
37}
38
39func runTLSStatusCmd(ctx context.Context, cp ControlPlane, out io.Writer, jsonOut bool) error {
40 status, err := cp.GetTLSStatus(ctx)
41 if err != nil {
42 return fmt.Errorf("failed to get TLS status: %w", err)
43 }
44 if status == nil {
45 return fmt.Errorf("failed to get TLS status: empty response")
46 }
47
48 if jsonOut {
49 return writeJSON(out, status)
50 }
51
52 return renderTLSStatusText(out, status)
53}
54
55func renderTLSStatusText(out io.Writer, s *dto.TLSStatusResponse) error {
56 if err := cliWriteLine(out, cliRenderTitle("Public TLS / ACME Status")); err != nil {
57 return err
58 }
59
60 if err := renderTLSModeSection(out, s); err != nil {
61 return err
62 }
63
64 if err := renderTLSCertificatesSection(out, s); err != nil {
65 return err
66 }
67
68 if err := renderTLSRouteCoverageSection(out, s); err != nil {
69 return err
70 }
71
72 return renderTLSErrorsSection(out, s)
73}
74
75func renderTLSModeSection(out io.Writer, s *dto.TLSStatusResponse) error {
76 acmeStatus := "disabled"
77 if s.ACMEEnabled {
78 acmeStatus = "enabled"
79 }
80 if err := cliWriteLine(out, cliRenderMeta("ACME:", acmeStatus)); err != nil {
81 return err
82 }
83 if err := cliWriteLine(out, cliRenderMeta("Configured Mode:", s.ConfiguredMode)); err != nil {
84 return err
85 }
86 if err := cliWriteLine(out, cliRenderMeta("Effective Mode:", s.EffectiveMode)); err != nil {
87 return err
88 }
89 if s.SelectionReason != "" {
90 if err := cliWriteLine(out, cliRenderMeta("Reason:", s.SelectionReason)); err != nil {
91 return err
92 }
93 }
94 return cliWriteLine(out, cliRenderMeta("Token Source:", s.TokenSource))
95}
96
97func renderTLSCertificatesSection(out io.Writer, s *dto.TLSStatusResponse) error {
98 if len(s.Certificates) == 0 {
99 return nil
100 }
101
102 if err := cliWriteLine(out, ""); err != nil {
103 return err
104 }
105 if err := cliWriteLine(out, cliRenderTitle("Certificates")); err != nil {
106 return err
107 }
108
109 for _, cert := range s.Certificates {
110 if err := cliWritef(out, " %s\n", cliRenderMeta("ID:", cert.ID)); err != nil {
111 return err
112 }
113 if err := cliWriteLine(out, cliRenderMeta(" Names:", strings.Join(cert.Names, ", "))); err != nil {
114 return err
115 }
116 if err := cliWriteLine(out, cliRenderMeta(" Status:", cert.Status)); err != nil {
117 return err
118 }
119 if !cert.NotAfter.IsZero() {
120 if err := cliWriteLine(out, cliRenderMeta(" Not After:", cert.NotAfter.Format(time.DateTime))); err != nil {
121 return err
122 }
123 }
124 if cert.LastError != "" {
125 if err := cliWriteLine(out, cliRenderMeta(" Last Error:", cert.LastError)); err != nil {
126 return err
127 }
128 }
129 }
130
131 return nil
132}
133
134func renderTLSRouteCoverageSection(out io.Writer, s *dto.TLSStatusResponse) error {
135 if len(s.Routes) == 0 {
136 return nil
137 }
138
139 if err := cliWriteLine(out, ""); err != nil {
140 return err
141 }
142 if err := cliWriteLine(out, cliRenderTitle("Route Coverage")); err != nil {
143 return err
144 }
145
146 for _, route := range s.Routes {
147 covered := "no"
148 if route.Covered {
149 covered = "yes"
150 }
151 if err := cliWritef(out, " %s covered=%s", route.Domain, covered); err != nil {
152 return err
153 }
154 if route.CoveredBy != "" {
155 if err := cliWritef(out, " covered_by=%s", route.CoveredBy); err != nil {
156 return err
157 }
158 }
159 if route.Error != "" {
160 if err := cliWritef(out, " error=%s", route.Error); err != nil {
161 return err
162 }
163 }
164 if err := cliWriteLine(out, ""); err != nil {
165 return err
166 }
167 }
168
169 return nil
170}
171
172func renderTLSErrorsSection(out io.Writer, s *dto.TLSStatusResponse) error {
173 if len(s.Errors) == 0 {
174 return nil
175 }
176
177 if err := cliWriteLine(out, ""); err != nil {
178 return err
179 }
180 if err := cliWriteLine(out, cliRenderTitle("Errors")); err != nil {
181 return err
182 }
183
184 for _, e := range s.Errors {
185 if err := cliWriteLine(out, " "+e); err != nil {
186 return err
187 }
188 }
189
190 return nil
191}