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