1+ // Package main provides SSL certificate management functionality using CertBot.
2+ // It supports both HTTP-01 and DNS-01 challenge types for certificate generation
3+ // and automatic renewal of SSL certificates.
14package main
25
36import (
@@ -15,42 +18,61 @@ import (
1518 "github.com/spf13/afero"
1619)
1720
21+ // Executor provides an interface for executing system commands.
22+ // This interface allows for easy mocking in tests and provides
23+ // a clean abstraction over the exec package.
24+ //
1825//go:generate mockery --name=Executor --filename=executor.go
1926type Executor interface {
27+ // Command creates a new *exec.Cmd with the given name and arguments.
2028 Command (name string , arg ... string ) * exec.Cmd
29+ // Run executes the given command and waits for it to complete.
2130 Run (cmd * exec.Cmd ) error
2231}
2332
33+ // executor is the default implementation of the Executor interface.
2434type executor struct {}
2535
36+ // NewExecutor creates a new Executor instance.
2637func NewExecutor () Executor {
2738 return & executor {}
2839}
2940
41+ // Command creates a new *exec.Cmd with the given name and arguments.
3042func (e * executor ) Command (name string , arg ... string ) * exec.Cmd {
3143 return exec .Command (name , arg ... )
3244}
3345
46+ // Run executes the given command and waits for it to complete.
3447func (e * executor ) Run (cmd * exec.Cmd ) error {
3548 return cmd .Run ()
3649}
3750
51+ // Ticker provides an interface for time-based operations with context support.
52+ // This interface allows for easy mocking in tests and provides a clean
53+ // abstraction over the time package's ticker functionality.
54+ //
3855//go:generate mockery --name=Ticker --filename=ticker.go
3956type Ticker interface {
40- // Init creates a new [time.Ticker] internally with [time.Duration] defined.
57+ // Init creates a new time.Ticker internally with the specified duration.
58+ // The ticker will respect the provided context for cancellation.
4159 Init (context.Context , time.Duration )
42- // Tick waits for a ticker's tick and return the value. If ticker wasn't initialized, a [time.Time] with zero-value
43- // will be returned .
60+ // Tick returns a channel that receives the current time on each tick.
61+ // If the ticker wasn't initialized, the channel will be nil .
4462 Tick () chan time.Time
45- // Stop stops the ticker initialized . If ticker wasn't initialized, nothing happens .
63+ // Stop stops the ticker. If the ticker wasn't initialized, this is a no-op .
4664 Stop ()
4765}
4866
67+ // ticker is the default implementation of the Ticker interface.
4968type ticker struct {
5069 ticker * time.Ticker
5170 tick chan time.Time
5271}
5372
73+ // Init creates a new time.Ticker internally with the specified duration.
74+ // It starts a goroutine that forwards ticker events to the tick channel
75+ // and handles context cancellation.
5476func (t * ticker ) Init (ctx context.Context , duration time.Duration ) {
5577 t .ticker = time .NewTicker (duration )
5678 t .tick = make (chan time.Time )
@@ -73,10 +95,12 @@ func (t *ticker) Init(ctx context.Context, duration time.Duration) {
7395 }()
7496}
7597
98+ // Tick returns a channel that receives the current time on each tick.
7699func (t * ticker ) Tick () chan time.Time {
77100 return t .tick
78101}
79102
103+ // Stop stops the ticker. If the ticker wasn't initialized, this is a no-op.
80104func (t * ticker ) Stop () {
81105 if t .ticker == nil {
82106 return
@@ -85,34 +109,49 @@ func (t *ticker) Stop() {
85109 t .ticker .Stop ()
86110}
87111
88- // DNSProvider represents a DNS provider to generate certificates.
112+ // DNSProvider represents a DNS provider that can be used for DNS-01 challenges
113+ // when generating SSL certificates.
89114type DNSProvider string
90115
91116// DigitalOceanDNSProvider represents the Digital Ocean DNS provider.
92117const DigitalOceanDNSProvider = "digitalocean"
93118
119+ // Config holds the configuration for CertBot operations.
94120type Config struct {
95- // RootDir is the root directory for CertBot configurations.
121+ // RootDir is the root directory where CertBot stores its configurations
122+ // and generated certificates. Typically "/etc/letsencrypt".
96123 RootDir string
97- // Staging defines if the CertBot will use the staging server to generate certificates.
124+ // Staging defines whether CertBot should use Let's Encrypt's staging server
125+ // instead of the production server. Useful for testing to avoid rate limits.
98126 Staging bool
99- // RenewedCallback is a callback called after certificate renew.
127+ // RenewedCallback is an optional callback function that gets called
128+ // after a certificate is successfully renewed.
100129 RenewedCallback func ()
101130}
102131
132+ // Certificate represents an SSL certificate that can be generated using CertBot.
103133type Certificate interface {
134+ // String returns a string representation of the certificate, typically the domain name.
104135 String () string
136+ // Generate creates the SSL certificate using CertBot.
137+ // The staging parameter determines whether to use Let's Encrypt's staging server.
105138 Generate (staging bool ) error
106139}
107140
141+ // DefaultCertificate represents a standard SSL certificate that uses HTTP-01 challenge
142+ // for domain validation. This is suitable for single domains where you have control
143+ // over the web server.
108144type DefaultCertificate struct {
145+ // RootDir is the root directory for certificate storage.
109146 RootDir string
110- Domain string
147+ // Domain is the domain name for which the certificate will be generated.
148+ Domain string
111149
112150 ex Executor
113151 fs afero.Fs
114152}
115153
154+ // NewDefaultCertificate creates a new DefaultCertificate instance for the given domain.
116155func NewDefaultCertificate (domain string ) Certificate {
117156 return & DefaultCertificate {
118157 Domain : domain ,
@@ -122,6 +161,9 @@ func NewDefaultCertificate(domain string) Certificate {
122161 }
123162}
124163
164+ // startACMEServer starts a local HTTP server on port 80 to handle ACME HTTP-01 challenges.
165+ // This server serves files from the .well-known/acme-challenge directory which is
166+ // required for Let's Encrypt domain validation.
125167func (d * DefaultCertificate ) startACMEServer () * http.Server {
126168 mux := http .NewServeMux ()
127169 mux .Handle (
@@ -152,23 +194,28 @@ func (d *DefaultCertificate) startACMEServer() *http.Server {
152194 return server
153195}
154196
155- // stopACMEServer stops the local ACME server.
197+ // stopACMEServer gracefully stops the local ACME HTTP server.
156198func (d * DefaultCertificate ) stopACMEServer (server * http.Server ) {
157199 if err := server .Close (); err != nil {
158200 log .WithError (err ).Fatal ("could not stop ACME server" )
159201 }
160202}
161203
204+ // Generate creates an SSL certificate for the domain using HTTP-01 challenge.
205+ // It starts a local HTTP server to handle the ACME challenge, runs CertBot,
206+ // and then stops the server.
162207func (d * DefaultCertificate ) Generate (staging bool ) error {
163208 log .Info ("generating SSL certificate" )
164209
210+ // Create the ACME challenge directory
165211 challengeDir := fmt .Sprintf ("%s/.well-known/acme-challenge" , os .TempDir ())
166212 if err := d .fs .MkdirAll (challengeDir , 0o755 ); err != nil {
167213 log .WithError (err ).Error ("failed to create acme challenge on filesystem" )
168214
169215 return err
170216 }
171217
218+ // Start the ACME server to handle HTTP-01 challenges
172219 acmeServer := d .startACMEServer ()
173220
174221 args := []string {
@@ -204,29 +251,36 @@ func (d *DefaultCertificate) Generate(staging bool) error {
204251 return err
205252 }
206253
254+ // Stop the ACME server
207255 d .stopACMEServer (acmeServer )
208256
209257 log .Info ("generate run" )
210258
211259 return nil
212260}
213261
262+ // String returns the domain name as the string representation of the certificate.
214263func (d * DefaultCertificate ) String () string {
215264 return d .Domain
216265}
217266
267+ // TunnelsCertificate represents a wildcard SSL certificate that uses DNS-01 challenge
268+ // for domain validation. This is suitable for wildcard certificates (*.example.com)
269+ // where you have control over the DNS records.
218270type TunnelsCertificate struct {
219- // Domain is the default domain used to generate certificate for Tunnels .
271+ // Domain is the base domain used to generate wildcard certificates .
220272 Domain string
221- // Provider is the DNS provider used to generate wildcard certificates .
273+ // Provider is the DNS provider used for DNS-01 challenges .
222274 Provider DNSProvider
223- // Token is a DNS token used to generate wildcard certificates .
275+ // Token is the API token for the DNS provider .
224276 Token string
225277
226278 ex Executor
227279 fs afero.Fs
228280}
229281
282+ // NewTunnelsCertificate creates a new TunnelsCertificate instance for generating
283+ // wildcard certificates using DNS-01 challenges.
230284func NewTunnelsCertificate (domain string , provider DNSProvider , token string ) Certificate {
231285 return & TunnelsCertificate {
232286 Domain : domain ,
@@ -239,6 +293,8 @@ func NewTunnelsCertificate(domain string, provider DNSProvider, token string) Ce
239293 }
240294}
241295
296+ // generateProviderCredentialsFile creates a credentials file for the DNS provider.
297+ // This file contains the API token needed for DNS-01 challenges.
242298func (d * TunnelsCertificate ) generateProviderCredentialsFile () (afero.File , error ) {
243299 token := fmt .Sprintf ("dns_%s_token = %s" , d .Provider , d .Token )
244300
@@ -258,16 +314,21 @@ func (d *TunnelsCertificate) generateProviderCredentialsFile() (afero.File, erro
258314 return file , nil
259315}
260316
317+ // Generate creates a wildcard SSL certificate for the domain using DNS-01 challenge.
318+ // It creates a credentials file for the DNS provider, runs CertBot with DNS plugin,
319+ // and generates a wildcard certificate.
261320func (d * TunnelsCertificate ) Generate (staging bool ) error {
262321 log .Info ("generating SSL certificate with DNS" )
263322
323+ // Create the DNS provider credentials file
264324 file , err := d .generateProviderCredentialsFile ()
265325 if err != nil {
266326 log .WithError (err ).Error ("failed to generate INI file" )
267327
268328 return err
269329 }
270330
331+ // Build the CertBot command arguments for DNS-01 challenge
271332 args := []string {
272333 "certonly" ,
273334 "--non-interactive" ,
@@ -306,21 +367,26 @@ func (d *TunnelsCertificate) Generate(staging bool) error {
306367 return nil
307368}
308369
370+ // String returns the domain name as the string representation of the certificate.
309371func (d * TunnelsCertificate ) String () string {
310372 return d .Domain
311373}
312374
313- // CertBot handles the generation and renewal of SSL certificates.
375+ // CertBot is the main structure that handles SSL certificate generation and renewal.
376+ // It manages multiple certificates and provides automatic renewal functionality.
314377type CertBot struct {
378+ // Config holds the configuration for CertBot operations.
315379 Config * Config
316380
381+ // Certificates is a list of certificates to manage.
317382 Certificates []Certificate
318383
319384 ex Executor
320385 tk Ticker
321386 fs afero.Fs
322387}
323388
389+ // newCertBot creates a new CertBot instance with the given configuration.
324390func newCertBot (config * Config ) * CertBot {
325391 return & CertBot {
326392 Config : config ,
@@ -331,7 +397,8 @@ func newCertBot(config *Config) *CertBot {
331397 }
332398}
333399
334- // ensureCertificates checks if the SSL certificate exists and generates if it doesn't.
400+ // ensureCertificates checks if SSL certificates exist for all managed domains.
401+ // If a certificate doesn't exist, it generates a new one.
335402func (cb * CertBot ) ensureCertificates () {
336403 for _ , certificate := range cb .Certificates {
337404 certPath := fmt .Sprintf ("%s/live/%s/fullchain.pem" , cb .Config .RootDir , certificate )
@@ -341,6 +408,8 @@ func (cb *CertBot) ensureCertificates() {
341408 }
342409}
343410
411+ // executeRenewCertificates runs the CertBot renew command to check and renew
412+ // certificates that are close to expiration.
344413func (cb * CertBot ) executeRenewCertificates () error {
345414 args := []string {
346415 "renew" ,
@@ -366,7 +435,9 @@ func (cb *CertBot) executeRenewCertificates() error {
366435 return nil
367436}
368437
369- // renewCertificates periodically renews the SSL certificates.
438+ // renewCertificates starts a background process that periodically checks and renews
439+ // SSL certificates. It runs in a loop with the specified duration between checks.
440+ // The process respects context cancellation for graceful shutdown.
370441func (cb * CertBot ) renewCertificates (ctx context.Context , duration time.Duration ) {
371442 log .Info ("starting SSL certificate renewal process" )
372443
0 commit comments