Major Refactoring, Client can now be used as a library
Some checks failed
Build (artifact) / build (push) Failing after 1m3s

This commit is contained in:
2026-03-16 21:48:32 +01:00
parent e6a2ba2f8b
commit e0f68788c0
45 changed files with 1359 additions and 1245 deletions

125
app/server/certs.go Normal file
View File

@@ -0,0 +1,125 @@
package server
import (
"fmt"
"path/filepath"
"time"
"git.nevets.tech/Keys/certman/app/shared"
"git.nevets.tech/Keys/certman/common"
"git.nevets.tech/Keys/certman/server"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/spf13/cobra"
)
var (
noPush bool
renewCertSubCmd = &cobra.Command{
Use: "renew",
Short: "Renews a domains certificate",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return renewCertCmd(args[0], noPush)
},
}
)
func init() {
renewCertSubCmd.Flags().BoolVar(&noPush, "no-push", false, "Don't push certs to repo, renew locally only [server mode only]")
shared.CertCmd.AddCommand(renewCertSubCmd)
}
func renewCertCmd(domain string, noPush bool) error {
if err := shared.LoadConfig(); err != nil {
return err
}
if err := shared.LoadDomainConfigs(); err != nil {
return err
}
mgr, err := server.NewACMEManager(shared.Config())
if err != nil {
return err
}
err = renewCerts(domain, noPush, mgr)
if err != nil {
return err
}
// return ReloadDaemonCmd() // Not sure if this is necessary
return nil
}
func renewCerts(domain string, noPush bool, mgr *server.ACMEManager) error {
config := shared.Config()
domainConfig, exists := shared.DomainStore().Get(domain)
if !exists {
return fmt.Errorf("domain %s does not exist", domain)
}
_, err := mgr.RenewForDomain(domain)
if err != nil {
// if no existing cert, obtain instead
_, err = mgr.ObtainForDomain(domain, config, domainConfig)
if err != nil {
return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err)
}
}
domainConfig.Internal.LastIssued = time.Now().UTC().Unix()
err = shared.WriteDomainConfig(domainConfig)
if err != nil {
return fmt.Errorf("error saving domain config %s: %v", domain, err)
}
err = common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(mgr.CertsRoot, domain, domain+".crt"), filepath.Join(mgr.CertsRoot, domain, domain+".crt.crpt"), nil)
if err != nil {
return fmt.Errorf("error encrypting domain cert for domain %s: %v", domain, err)
}
err = common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(mgr.CertsRoot, domain, domain+".key"), filepath.Join(mgr.CertsRoot, domain, domain+".key.crpt"), nil)
if err != nil {
return fmt.Errorf("error encrypting domain key for domain %s: %v", domain, err)
}
if !noPush {
giteaClient := common.CreateGiteaClient(config)
if giteaClient == nil {
return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err)
}
gitWorkspace := &common.GitWorkspace{
Storage: memory.NewStorage(),
FS: memfs.New(),
}
var repoUrl string
if !domainConfig.Internal.RepoExists {
repoUrl = common.CreateGiteaRepo(domain, giteaClient, config, domainConfig)
if repoUrl == "" {
return fmt.Errorf("error creating Gitea repo for domain %s", domain)
}
domainConfig.Internal.RepoExists = true
err = shared.WriteDomainConfig(domainConfig)
if err != nil {
return fmt.Errorf("error saving domain config %s: %v", domain, err)
}
err = common.InitRepo(repoUrl, gitWorkspace)
if err != nil {
return fmt.Errorf("error initializing repo for domain %s: %v", domain, err)
}
} else {
repoUrl = config.Git.Server + "/" + config.Git.OrgName + "/" + domain + domainConfig.Repo.RepoSuffix + ".git"
err = common.CloneRepo(repoUrl, gitWorkspace, common.Server, config)
if err != nil {
return fmt.Errorf("error cloning repo for domain %s: %v", domain, err)
}
}
err = common.AddAndPushCerts(domain, gitWorkspace, config, domainConfig)
if err != nil {
return fmt.Errorf("error pushing certificates for domain %s: %v", domain, err)
}
fmt.Printf("Successfully pushed certificates for domain %s\n", domain)
}
return nil
}

168
app/server/daemon.go Normal file
View File

@@ -0,0 +1,168 @@
package server
import (
"fmt"
"log"
"path/filepath"
"sync"
"time"
appShared "git.nevets.tech/Keys/certman/app/shared"
"git.nevets.tech/Keys/certman/common"
"git.nevets.tech/Keys/certman/server"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
)
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
}
renewPeriod := domainConfig.Certificates.RenewPeriod
lastIssued := time.Unix(domainConfig.Internal.LastIssued, 0).UTC()
renewalDue := lastIssued.AddDate(0, 0, renewPeriod)
if now.After(renewalDue) {
//TODO extra check if certificate expiry (create cache?)
_, err := d.ACMEManager.RenewForDomain(domainStr)
if err != nil {
// if no existing cert, obtain instead
_, err = d.ACMEManager.ObtainForDomain(domainStr, appShared.Config(), domainConfig)
if err != nil {
fmt.Printf("Error obtaining domain certificates for domain %s: %v\n", domainStr, err)
continue
}
}
domainConfig.Internal.LastIssued = time.Now().UTC().Unix()
err = appShared.WriteDomainConfig(domainConfig)
if err != nil {
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
continue
}
err = common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(d.ACMEManager.CertsRoot, domainStr, domainStr+".crt"), filepath.Join(d.ACMEManager.CertsRoot, domainStr, domainStr+".crt.crpt"), nil)
if err != nil {
fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err)
continue
}
err = common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(d.ACMEManager.CertsRoot, domainStr, domainStr+".key"), filepath.Join(d.ACMEManager.CertsRoot, domainStr, domainStr+".key.crpt"), nil)
if err != nil {
fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err)
continue
}
giteaClient := common.CreateGiteaClient(config)
if giteaClient == nil {
fmt.Printf("Error creating gitea client for domain %s: %v\n", domainStr, err)
continue
}
gitWorkspace := &common.GitWorkspace{
Storage: memory.NewStorage(),
FS: memfs.New(),
}
var repoUrl string
if !domainConfig.Internal.RepoExists {
repoUrl = common.CreateGiteaRepo(domainStr, giteaClient, config, domainConfig)
if repoUrl == "" {
fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr)
continue
}
domainConfig.Internal.RepoExists = true
err = appShared.WriteDomainConfig(domainConfig)
if err != nil {
fmt.Printf("Error saving domain config %s: %v\n", domainStr, err)
continue
}
err = common.InitRepo(repoUrl, gitWorkspace)
if err != nil {
fmt.Printf("Error initializing repo for domain %s: %v\n", domainStr, err)
continue
}
} else {
repoUrl = appShared.Config().Git.Server + "/" + appShared.Config().Git.OrgName + "/" + domainStr + domainConfig.Repo.RepoSuffix + ".git"
err = common.CloneRepo(repoUrl, gitWorkspace, common.Server, config)
if err != nil {
fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err)
continue
}
}
err = common.AddAndPushCerts(domainStr, gitWorkspace, config, domainConfig)
if 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)
}
}
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")
}

1
app/server/server.go Normal file
View File

@@ -0,0 +1 @@
package server