Files
certman/app/server/daemon.go

175 lines
4.9 KiB
Go

package main
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"time"
appShared "git.nevets.tech/Steven/certman/app"
"git.nevets.tech/Steven/certman/common"
"git.nevets.tech/Steven/certman/server"
)
type Daemon struct {
ACMEManager *server.ACMEManager
TickMu sync.Mutex
MgrMu sync.Mutex
}
func (d *Daemon) loadACMEManager() error {
d.MgrMu.Lock()
defer d.MgrMu.Unlock()
if d.ACMEManager == nil {
var err error
d.ACMEManager, err = server.NewACMEManager(appShared.Config())
if err != nil {
return err
}
}
return nil
}
func (d *Daemon) Init() {
fmt.Println("Starting CertManager in server mode...")
err := appShared.LoadDomainConfigs()
if err != nil {
log.Fatalf("Error loading domain configs: %v", err)
}
d.Tick()
}
func (d *Daemon) Tick() {
d.TickMu.Lock()
defer d.TickMu.Unlock()
fmt.Println("Tick!")
if err := d.loadACMEManager(); err != nil {
fmt.Printf("Error getting acme manager: %v\n", err)
return
}
now := time.Now().UTC()
config := appShared.Config()
localDomainConfigs := appShared.DomainStore().Snapshot()
for domainStr, domainConfig := range localDomainConfigs {
if !domainConfig.Domain.Enabled {
continue
}
//TODO: have renewPeriod logic default to use certificate expiry if available
renewPeriod := domainConfig.Certificates.RenewPeriod
lastIssued := time.Unix(domainConfig.Internal.LastIssued, 0).UTC()
renewalDue := lastIssued.AddDate(0, 0, renewPeriod)
if !now.After(renewalDue) {
continue
}
//TODO extra check if certificate expiry (create cache?)
if _, err := d.ACMEManager.RenewForDomain(domainStr); err != nil {
if errors.Is(err, os.ErrNotExist) {
if _, err := d.ACMEManager.ObtainForDomain(domainStr, config, domainConfig); err != nil {
fmt.Printf("Error obtaining domain certificates for domain %s: %v\n", domainStr, err)
continue
}
} else {
fmt.Printf("Error: %v\n", err)
continue
}
}
domainConfig.Internal.LastIssued = time.Now().UTC().Unix()
if err := appShared.WriteDomainConfig(domainConfig); err != nil {
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
continue
}
certsDir := filepath.Join(d.ACMEManager.CertsRoot, domainStr)
if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domainStr+".crt"), filepath.Join(certsDir, domainStr+".crt.crpt"), nil); err != nil {
fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err)
continue
}
if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domainStr+".key"), filepath.Join(certsDir, domainStr+".key.crpt"), nil); err != nil {
fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err)
continue
}
ws, err := prepareServerWorkspace(config, domainConfig, domainStr)
if err != nil {
fmt.Printf("Error preparing git workspace for domain %s: %v\n", domainStr, err)
continue
}
if err := server.AddAndPushCerts(ws, certsDir, config); err != nil {
fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err)
continue
}
fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr)
}
if err := appShared.SaveDomainConfigs(); err != nil {
fmt.Printf("Error saving domain configs: %v\n", err)
}
}
// prepareServerWorkspace creates or clones the domain's remote repo into a
// fresh in-memory workspace. If the repo is being cloned, it verifies that
// SERVER_ID matches this server's UUID (or is absent, in which case the
// domain is adopted on the next push).
func prepareServerWorkspace(config *common.AppConfig, domainConfig *common.DomainConfig, domain string) (*common.GitWorkspace, error) {
if !domainConfig.Internal.RepoExists {
url, err := server.CreateRepo(config, domainConfig, domain)
if err != nil {
return nil, fmt.Errorf("create remote repo: %w", err)
}
domainConfig.Internal.RepoExists = true
if err := appShared.WriteDomainConfig(domainConfig); err != nil {
return nil, fmt.Errorf("save domain config: %w", err)
}
ws := common.NewGitWorkspace(domain, url)
if err := common.InitRepo(ws); err != nil {
return nil, fmt.Errorf("init workspace: %w", err)
}
return ws, nil
}
url := common.RepoURL(config, domainConfig, domain)
ws := common.NewGitWorkspace(domain, url)
if err := common.CloneRepo(ws, config); err != nil {
return nil, fmt.Errorf("clone: %w", err)
}
owned, err := server.VerifyOwnership(ws, config.App.UUID)
if err != nil {
return nil, err
}
if !owned {
fmt.Printf("Adopting unclaimed repo for domain %s\n", domain)
}
return ws, nil
}
func (d *Daemon) Reload() {
fmt.Println("Reloading configs...")
err := appShared.LoadDomainConfigs()
if err != nil {
fmt.Printf("Error loading domain configs: %v\n", err)
return
}
d.MgrMu.Lock()
d.ACMEManager = nil
d.MgrMu.Unlock()
fmt.Println("Successfully reloaded configs")
}
func (d *Daemon) Stop() {
fmt.Println("Shutting down server")
}