[CI-SKIP] Upload current

This commit is contained in:
2026-04-24 10:37:46 -04:00
parent 6aacbfbb71
commit fb1abd6211
12 changed files with 519 additions and 579 deletions

View File

@@ -2,13 +2,10 @@ package main
import (
"fmt"
"path/filepath"
"git.nevets.tech/Steven/certman/app"
"git.nevets.tech/Steven/certman/client"
"git.nevets.tech/Steven/certman/common"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/spf13/cobra"
)
@@ -47,21 +44,18 @@ func init() {
}
func renewCert(domain string) error {
gitWorkspace := &common.GitWorkspace{
Domain: domain,
Storage: memory.NewStorage(),
FS: memfs.New(),
}
config := app.Config()
domainConfig, exists := app.DomainStore().Get(domain)
if !exists {
return app.ErrConfigNotFound
}
if err := client.PullCerts(config, domainConfig, gitWorkspace); err != nil {
return err
url := common.RepoURL(config, domainConfig, domain)
ws := common.NewGitWorkspace(domain, url)
if err := common.CloneRepo(ws, config); err != nil {
return fmt.Errorf("clone %s: %w", domain, err)
}
certsDir := common.EffectiveDataRoot(config, domainConfig)
return client.DecryptAndWriteCertificates(certsDir, config, domainConfig, gitWorkspace)
certsDir := common.CertsDir(config, domainConfig, domain)
return client.DecryptAndWriteCertificates(certsDir, domainConfig, ws)
}
func updateLinks(domain string) error {
@@ -69,26 +63,6 @@ func updateLinks(domain string) error {
if !exists {
return fmt.Errorf("domain %s does not exist", domain)
}
effectiveDataRoot := common.EffectiveDataRoot(app.Config(), domainConfig)
certsDir := filepath.Join(effectiveDataRoot, "certificates", domain)
certLinks := domainConfig.Certificates.CertSymlinks
for _, certLink := range certLinks {
err := common.LinkFile(filepath.Join(certsDir, domain+".crt"), certLink, domain, ".crt")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v", certLink, domain, err)
continue
}
}
keyLinks := domainConfig.Certificates.KeySymlinks
for _, keyLink := range keyLinks {
err := common.LinkFile(filepath.Join(certsDir, domain+".key"), keyLink, domain, ".key")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v", keyLink, domain, err)
continue
}
}
return nil
certsDir := common.CertsDir(app.Config(), domainConfig, domain)
return client.UpdateSymlinks(domain, domainConfig, certsDir)
}

View File

@@ -1,161 +1,84 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"path/filepath"
"strings"
"git.nevets.tech/Steven/certman/app"
"git.nevets.tech/Steven/certman/client"
"git.nevets.tech/Steven/certman/common"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
)
type Daemon struct{}
func (d *Daemon) Init() {
fmt.Println("Starting CertManager in client mode...")
err := app.LoadDomainConfigs()
if err != nil {
if err := app.LoadDomainConfigs(); err != nil {
log.Fatalf("Error loading domain configs: %v", err)
}
d.Tick()
}
func (d *Daemon) Tick() {
fmt.Println("tick!")
// Get local copy of configs
config := app.Config()
localDomainConfigs := app.DomainStore().Snapshot()
// Loop over all domain configs (domains)
for domainStr, domainConfig := range localDomainConfigs {
// Skip non-enabled domains
if !domainConfig.Domain.Enabled {
continue
}
// Skip domains with up-to-date commit hashes
// If the repo doesn't exist, we can't check for a remote commit, so stop the rest of the check
repoExists := domainConfig.Internal.RepoExists
if repoExists {
dataRoot := common.EffectiveDataRoot(config, domainConfig)
localHash, err := client.LocalCommitHash(domainStr, dataRoot)
certsDir := common.CertsDir(config, domainConfig, domainStr)
// Short-circuit when the local copy already matches the remote HEAD.
// Only useful once the server has provisioned the repo; otherwise
// the RemoteCommitHash call returns ErrRepoNotFound and we skip
// this tick entirely (nothing to pull yet).
if domainConfig.Internal.RepoExists {
localHash, err := client.LocalCommitHash(certsDir)
if err != nil {
fmt.Printf("No local commit hash found for domain %s\n", domainStr)
fmt.Printf("Error reading local hash for %s: %v\n", domainStr, err)
}
gitSource, err := common.StrToGitSource(app.Config().Git.Host)
remoteHash, err := client.RemoteCommitHash(config, domainConfig, domainStr)
if err != nil {
fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err)
if errors.Is(err, common.ErrRepoNotFound) {
fmt.Printf("Remote repo not yet provisioned for %s; skipping\n", domainStr)
continue
}
fmt.Printf("Error getting remote hash for %s: %v\n", domainStr, err)
continue
}
remoteHash, err := client.RemoteCommitHash(domainStr, gitSource, config, domainConfig)
if err != nil {
fmt.Printf("Error getting remote commit hash for domain %s: %v\n", domainStr, err)
}
// If both hashes are blank (errored), break
// If localHash equals remoteHash (local is up-to-date), skip
if !(localHash == "" && remoteHash == "") && localHash == remoteHash {
if localHash != "" && localHash == remoteHash {
fmt.Printf("Domain %s is up to date. Skipping...\n", domainStr)
continue
}
}
gitWorkspace := &common.GitWorkspace{
Storage: memory.NewStorage(),
FS: memfs.New(),
}
// Ex: https://git.example.com/Org/Repo-suffix.git
// Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?)
repoUrl := app.Config().Git.Server + "/" + config.Git.OrgName + "/" + domainStr + domainConfig.Repo.RepoSuffix + ".git"
err := common.CloneRepo(repoUrl, gitWorkspace, common.Client, config)
if err != nil {
url := common.RepoURL(config, domainConfig, domainStr)
ws := common.NewGitWorkspace(domainStr, url)
if err := common.CloneRepo(ws, config); err != nil {
fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err)
continue
}
effectiveDataRoot := common.EffectiveDataRoot(config, domainConfig)
certsDir := filepath.Join(effectiveDataRoot, "certificates", domainStr)
// Get files in repo
fileInfos, err := gitWorkspace.FS.ReadDir("/")
if err != nil {
fmt.Printf("Error reading directory in memFS on domain %s: %v\n", domainStr, err)
if err := client.DecryptAndWriteCertificates(certsDir, domainConfig, ws); err != nil {
fmt.Printf("Error decrypting certificates for %s: %v\n", domainStr, err)
continue
}
// Iterate over files, filtering by .crpt (encrypted) files in case other files were accidentally added
for _, fileInfo := range fileInfos {
if strings.HasSuffix(fileInfo.Name(), ".crpt") {
filename, _ := strings.CutSuffix(fileInfo.Name(), ".crpt")
file, err := gitWorkspace.FS.Open(fileInfo.Name())
if err != nil {
fmt.Printf("Error opening file in memFS on domain %s: %v\n", domainStr, err)
continue
}
fileBytes, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Error reading file in memFS on domain %s: %v\n", domainStr, err)
file.Close()
continue
}
err = file.Close()
if err != nil {
fmt.Printf("Error closing file on domain %s: %v\n", domainStr, err)
continue
}
err = common.DecryptFileFromBytes(domainConfig.Certificates.CryptoKey, fileBytes, filepath.Join(certsDir, filename), nil)
if err != nil {
fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domainStr, err)
continue
}
headRef, err := gitWorkspace.Repo.Head()
if err != nil {
fmt.Printf("Error getting head reference for domain %s: %v\n", domainStr, err)
continue
}
err = common.WriteCommitHash(headRef.Hash().String(), config, domainConfig)
if err != nil {
fmt.Printf("Error writing commit hash: %v\n", err)
continue
}
certLinks := domainConfig.Certificates.CertSymlinks
for _, certLink := range certLinks {
err = common.LinkFile(filepath.Join(certsDir, domainStr+".crt"), certLink, domainStr, ".crt")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domainStr, err)
continue
}
}
keyLinks := domainConfig.Certificates.KeySymlinks
for _, keyLink := range keyLinks {
err = common.LinkFile(filepath.Join(certsDir, domainStr+".key"), keyLink, domainStr, ".key")
if err != nil {
fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domainStr, err)
continue
}
}
}
if err := client.UpdateSymlinks(domainStr, domainConfig, certsDir); err != nil {
fmt.Printf("Error updating symlinks for %s: %v\n", domainStr, err)
continue
}
}
}
func (d *Daemon) Reload() {
fmt.Println("Reloading configs...")
err := app.LoadDomainConfigs()
if err != nil {
if err := app.LoadDomainConfigs(); err != nil {
fmt.Printf("Error loading domain configs: %v\n", err)
return
}
}

View File

@@ -8,8 +8,6 @@ import (
"git.nevets.tech/Steven/certman/app"
"git.nevets.tech/Steven/certman/common"
"git.nevets.tech/Steven/certman/server"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/spf13/cobra"
)
@@ -41,12 +39,7 @@ func renewCertCmd(domain string, noPush bool) error {
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
return renewCerts(domain, noPush, mgr)
}
func renewCerts(domain string, noPush bool, mgr *server.ACMEManager) error {
@@ -56,70 +49,37 @@ func renewCerts(domain string, noPush bool, mgr *server.ACMEManager) error {
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 {
if _, err := mgr.RenewForDomain(domain); err != nil {
// If the domain has no stored resource yet, fall through to Obtain.
if _, err := mgr.ObtainForDomain(domain, config, domainConfig); err != nil {
return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err)
}
}
domainConfig.Internal.LastIssued = time.Now().UTC().Unix()
err = app.WriteDomainConfig(domainConfig)
if err != nil {
if err := app.WriteDomainConfig(domainConfig); 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 {
certsDir := filepath.Join(mgr.CertsRoot, domain)
if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domain+".crt"), filepath.Join(certsDir, domain+".crt.crpt"), nil); 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 {
if err := common.EncryptFileXChaCha(domainConfig.Certificates.CryptoKey, filepath.Join(certsDir, domain+".key"), filepath.Join(certsDir, domain+".key.crpt"), nil); 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 = app.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)
if noPush {
return nil
}
ws, err := prepareServerWorkspace(config, domainConfig, domain)
if err != nil {
return fmt.Errorf("prepare workspace for %s: %w", domain, err)
}
if err := server.AddAndPushCerts(ws, certsDir, config); err != nil {
return fmt.Errorf("push certificates for %s: %w", domain, err)
}
fmt.Printf("Successfully pushed certificates for domain %s\n", domain)
return nil
}

View File

@@ -12,8 +12,6 @@ import (
appShared "git.nevets.tech/Steven/certman/app"
"git.nevets.tech/Steven/certman/common"
"git.nevets.tech/Steven/certman/server"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5/storage/memory"
)
type Daemon struct {
@@ -69,91 +67,92 @@ func (d *Daemon) Tick() {
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 errors.Is(err, os.ErrNotExist) {
// 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
}
}
fmt.Printf("Error: %v\n", err)
continue
}
if !now.After(renewalDue) {
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)
//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 {
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)
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr)
}
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()