diff --git a/Makefile b/Makefile index 38a05a3..bcc5def 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ -VERSION := 1.0.0 +VERSION := 1.0.0-beta +BUILD := $(shell git rev-parse --short HEAD) GO := go BUILD_FLAGS := -buildmode=pie -trimpath -LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now" +LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now" -X git.nevets.tech/Keys/CertManager/internal.Version=$(VERSION) -X git.nevets.tech/Keys/CertManager/internal.Build=$(BUILD) build: - $(GO) build $(BUILD_FLAGS) -ldflags='$(LDFLAGS)' -o ./certman . + $(GO) build $(BUILD_FLAGS) -ldflags="$(LDFLAGS)" -o ./certman . @cp ./certman ./certman-$(VERSION)-amd64 stage: build diff --git a/client.go b/client/client.go similarity index 74% rename from client.go rename to client/client.go index c186707..b67c0a4 100644 --- a/client.go +++ b/client/client.go @@ -1,4 +1,4 @@ -package main +package client import ( "fmt" @@ -7,24 +7,26 @@ import ( "path/filepath" "strings" + "git.nevets.tech/Keys/CertManager/internal" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5/storage/memory" ) -func initClient() { - err := LoadDomainConfigs() +func Init() { + err := internal.LoadDomainConfigs() if err != nil { log.Fatalf("Error loading domain configs: %v", err) } - clientTick() + Tick() } -func clientTick() { +func Tick() { fmt.Println("Tick!") - // Get local copy of domain configs - localDomainConfigs := domainStore.Snapshot() + // Get local copy of configs + config := internal.Config() + localDomainConfigs := internal.DomainStore().Snapshot() // Loop over all domain configs (domains) for domainStr, domainConfig := range localDomainConfigs { @@ -37,16 +39,16 @@ func clientTick() { // If the repo doesn't exist, we can't check for a remote commit, so stop the rest of the check repoExists := domainConfig.GetBool("Internal.repo_exists") if repoExists { - localHash, err := getLocalCommitHash(domainStr) + localHash, err := internal.LocalCommitHash(domainStr) if err != nil { fmt.Printf("No local commit hash found for domain %s\n", domainStr) } - gitSource, err := strToGitSource(config.GetString("Git.host")) + gitSource, err := internal.StrToGitSource(internal.Config().GetString("Git.host")) if err != nil { fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err) continue } - remoteHash, err := getRemoteCommitHash(domainStr, gitSource) + remoteHash, err := internal.RemoteCommitHash(domainStr, gitSource) if err != nil { fmt.Printf("Error getting remote commit hash for domain %s: %v\n", domainStr, err) } @@ -58,20 +60,20 @@ func clientTick() { } } - gitWorkspace := &GitWorkspace{ + gitWorkspace := &internal.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 := config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" - err := cloneRepo(repoUrl, gitWorkspace) + repoUrl := internal.Config().GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" + err := internal.CloneRepo(repoUrl, gitWorkspace, internal.Client) if err != nil { fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err) continue } - certsDir, err := getDomainCertsDirWConf(domainStr, domainConfig) + certsDir, err := internal.DomainCertsDirWConf(domainStr, domainConfig) if err != nil { fmt.Printf("Error getting certificates dir for domain %s: %v\n", domainStr, err) continue @@ -104,7 +106,7 @@ func clientTick() { continue } - err = DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) + err = internal.DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) if err != nil { fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domainStr, err) continue @@ -116,7 +118,7 @@ func clientTick() { continue } - err = writeCommitHash(headRef.Hash().String(), domainConfig) + err = internal.WriteCommitHash(headRef.Hash().String(), domainConfig) if err != nil { fmt.Printf("Error writing commit hash: %v\n", err) continue @@ -124,7 +126,7 @@ func clientTick() { certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") for _, certLink := range certLinks { - err = linkFile(filepath.Join(certsDir, domainStr+".crt"), certLink, domainStr, ".crt") + err = internal.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 @@ -133,7 +135,7 @@ func clientTick() { keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") for _, keyLink := range keyLinks { - err = linkFile(filepath.Join(certsDir, domainStr+".crt"), keyLink, domainStr, ".key") + err = internal.LinkFile(filepath.Join(certsDir, domainStr+".crt"), keyLink, domainStr, ".key") if err != nil { fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domainStr, err) continue @@ -144,16 +146,16 @@ func clientTick() { } } -func reloadClient() { +func Reload() { fmt.Println("Reloading configs...") - err := LoadDomainConfigs() + err := internal.LoadDomainConfigs() if err != nil { fmt.Printf("Error loading domain configs: %v\n", err) return } } -func stopClient() { +func Stop() { fmt.Println("Shutting down client") } diff --git a/commands.go b/commands.go deleted file mode 100644 index 40f7edd..0000000 --- a/commands.go +++ /dev/null @@ -1,494 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io" - "log" - "os" - "os/exec" - "os/signal" - "os/user" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5/storage/memory" - "github.com/spf13/cobra" -) - -func devFunc(cmd *cobra.Command, args []string) { - testDomain := "lunamc.org" - //config, err = ezconf.LoadConfiguration("/etc/certman/certman.conf") - err := LoadConfig("/etc/certman/certman.conf") - if err != nil { - log.Fatalf("Error loading configuration: %v\n", err) - } - err = LoadDomainConfigs() - if err != nil { - log.Fatalf("Error loading configs: %v\n", err) - } - - fmt.Println(testDomain) -} - -func versionResponse(cmd *cobra.Command, args []string) { - fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build) -} - -func newKey(cmd *cobra.Command, args []string) { - key, err := GenerateKey() - if err != nil { - log.Fatalf("%v", err) - } - fmt.Printf(key) -} - -func newDomain(domain, domainDir string, dirOverridden bool) error { - //TODO add config option for "overriden dir" - fmt.Printf("Creating new domain %s\n", domain) - err := createNewDomainConfig(domain) - if err != nil { - return err - } - createNewDomainCertsDir(domain, domainDir, dirOverridden) - - certmanUser, err := user.Lookup("certman") - if err != nil { - return fmt.Errorf("error getting user certman: %v", err) - } - uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid)) - if err != nil { - return err - } - gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid)) - if err != nil { - return err - } - err = ChownRecursive("/etc/certman/domains", uid, gid) - if err != nil { - return err - } - err = ChownRecursive("/var/local/certman", uid, gid) - if err != nil { - return err - } - - fmt.Println("Successfully created domain entry for " + domain + "\nUpdate config file as needed in /etc/certman/domains/" + domain + ".conf\n") - return nil -} - -func install(isThin bool, mode string) error { - if !isThin { - if os.Geteuid() != 0 { - return fmt.Errorf("installation must be run as root") - } - - makeDirs() - createNewConfig(mode) - - f, err := os.OpenFile("/var/run/certman.pid", os.O_RDONLY|os.O_CREATE, 0755) - if err != nil { - return fmt.Errorf("error creating pid file: %v", err) - } - err = f.Close() - if err != nil { - return fmt.Errorf("error closing pid file: %v", err) - } - - newUserCmd := exec.Command("useradd", "-d", "/var/local/certman", "-U", "-r", "-s", "/sbin/nologin", "certman") - if output, err := newUserCmd.CombinedOutput(); err != nil { - return fmt.Errorf("error creating user: %v: output %s", err, output) - } - certmanUser, err := user.Lookup("certman") - if err != nil { - return fmt.Errorf("error getting user certman: %v", err) - } - uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid)) - if err != nil { - return err - } - gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid)) - if err != nil { - return err - } - err = ChownRecursive("/etc/certman", uid, gid) - if err != nil { - return fmt.Errorf("error changing uid/gid: %v", err) - } - err = ChownRecursive("/var/local/certman", uid, gid) - if err != nil { - return fmt.Errorf("error changing uid/gid: %v", err) - } - err = os.Chown("/var/run/certman.pid", uid, gid) - if err != nil { - return fmt.Errorf("error changing uid/gid: %v", err) - } - } else { - createNewConfig(mode) - } - return nil -} - -func renewCertFunc(domain string, noPush bool) error { - err := LoadConfig("/etc/certman/certman.conf") - if err != nil { - return err - } - err = LoadDomainConfigs() - if err != nil { - return err - } - switch config.GetString("App.mode") { - case "server": - mgr, err = NewACMEManager() - if err != nil { - return err - } - err = renewCerts(domain, noPush) - if err != nil { - return err - } - return reloadDaemon() - case "client": - return pullCerts(domain) - default: - return fmt.Errorf("invalid operating mode %s", config.GetString("App.mode")) - } -} - -func renewCerts(domain string, noPush bool) error { - _, err := mgr.RenewForDomain(domain) - if err != nil { - // if no existing cert, obtain instead - _, err = mgr.ObtainForDomain(domain) - if err != nil { - return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err) - } - } - - domainConfig, exists := domainStore.Get(domain) - if !exists { - return fmt.Errorf("domain %s does not exist", domain) - } - - domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix()) - err = WriteDomainConfig(domainConfig) - if err != nil { - return fmt.Errorf("error saving domain config %s: %v", domain, err) - } - - err = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), 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 = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), 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 := createGiteaClient() - if giteaClient == nil { - return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err) - } - gitWorkspace := &GitWorkspace{ - Storage: memory.NewStorage(), - FS: memfs.New(), - } - - var repoUrl string - if !domainConfig.GetBool("Internal.repo_exists") { - repoUrl = createGiteaRepo(domain, giteaClient) - if repoUrl == "" { - return fmt.Errorf("error creating Gitea repo for domain %s", domain) - } - domainConfig.Set("Internal.repo_exists", true) - err = WriteDomainConfig(domainConfig) - if err != nil { - return fmt.Errorf("error saving domain config %s: %v", domain, err) - } - - err = initRepo(repoUrl, gitWorkspace) - if err != nil { - return fmt.Errorf("error initializing repo for domain %s: %v", domain, err) - } - } else { - repoUrl = config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" - err = cloneRepo(repoUrl, gitWorkspace) - if err != nil { - return fmt.Errorf("error cloning repo for domain %s: %v", domain, err) - } - } - - err = addAndPushCerts(domain, gitWorkspace) - 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 -} - -func pullCerts(domain string) error { - gitWorkspace := &GitWorkspace{ - Storage: memory.NewStorage(), - FS: memfs.New(), - } - - domainConfig, exists := domainStore.Get(domain) - if !exists { - return fmt.Errorf("domain %s does not exist", domain) - } - - // Ex: https://git.example.com/Org/Repo-suffix.git - // Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?) - repoUrl := config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" - err := cloneRepo(repoUrl, gitWorkspace) - if err != nil { - return fmt.Errorf("Error cloning domain repo %s: %v\n", domain, err) - } - - certsDir, err := getDomainCertsDirWConf(domain, domainConfig) - if err != nil { - return fmt.Errorf("Error getting certificates dir for domain %s: %v\n", domain, err) - } - - // Get files in repo - fileInfos, err := gitWorkspace.FS.ReadDir("/") - if err != nil { - return fmt.Errorf("Error reading directory in memFS on domain %s: %v\n", domain, err) - } - // 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", domain, err) - continue - } - fileBytes, err := io.ReadAll(file) - if err != nil { - fmt.Printf("Error reading file in memFS on domain %s: %v\n", domain, err) - file.Close() - continue - } - err = file.Close() - if err != nil { - fmt.Printf("Error closing file on domain %s: %v\n", domain, err) - continue - } - - err = DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) - if err != nil { - fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domain, err) - continue - } - - headRef, err := gitWorkspace.Repo.Head() - if err != nil { - fmt.Printf("Error getting head reference for domain %s: %v\n", domain, err) - continue - } - - err = writeCommitHash(headRef.Hash().String(), domainConfig) - if err != nil { - fmt.Printf("Error writing commit hash: %v\n", err) - continue - } - - certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") - for _, certLink := range certLinks { - if certLink == "" { - continue - } - linkInfo, err := os.Stat(certLink) - if err != nil { - if !os.IsNotExist(err) { - fmt.Printf("Error stating cert link %s: %v\n", certLink, err) - continue - } - } - if linkInfo.IsDir() { - certLink = filepath.Join(certLink, domain+".crt") - } - - err = os.Link(filepath.Join(certsDir, domain+".crt"), certLink) - if err != nil { - fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domain, err) - continue - } - } - - keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") - for _, keyLink := range keyLinks { - if keyLink == "" { - continue - } - linkInfo, err := os.Stat(keyLink) - if err != nil { - if !os.IsNotExist(err) { - fmt.Printf("Error stating key link %s: %v\n", keyLink, err) - continue - } - } - if linkInfo.IsDir() { - keyLink = filepath.Join(keyLink, domain+".crt") - } - - err = os.Link(filepath.Join(certsDir, domain+".crt"), keyLink) - if err != nil { - fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domain, err) - continue - } - } - } - } - return nil -} - -func runDaemon() error { - err := createOrUpdatePIDFile("/var/run/certman.pid") - if err != nil { - if errors.Is(err, ErrorPIDInUse) { - return fmt.Errorf("daemon process is already running") - } - return fmt.Errorf("error creating pidfile: %v", err) - } - - ctx, cancel = context.WithCancel(context.Background()) - - // Check if main config exists - if _, err := os.Stat(configFile); os.IsNotExist(err) { - return fmt.Errorf("main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf") - } else if err != nil { - return fmt.Errorf("error opening %s: %v", configFile, err) - } - err = LoadConfig(configFile) - if err != nil { - return fmt.Errorf("error loading configuration: %v", err) - } - - // Setup SIGINT and SIGTERM listeners - sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM) - defer signal.Stop(sigChannel) - - reloadSigChan := make(chan os.Signal, 1) - signal.Notify(reloadSigChan, syscall.SIGHUP) - defer signal.Stop(reloadSigChan) - - tickSigChan := make(chan os.Signal, 1) - signal.Notify(tickSigChan, syscall.SIGUSR1) - defer signal.Stop(tickSigChan) - - tickRate := config.GetInt("App.tick_rate") - ticker := time.NewTicker(time.Duration(tickRate) * time.Hour) - defer ticker.Stop() - - wg.Add(1) - if config.GetString("App.mode") == "server" { - fmt.Println("Starting CertManager in server mode...") - // Server Task loop - go func() { - initServer() - defer wg.Done() - for { - select { - case <-ctx.Done(): - stopServer() - return - case <-reloadSigChan: - reloadServer() - case <-ticker.C: - serverTick() - case <-tickSigChan: - serverTick() - } - } - }() - } else if config.GetString("App.mode") == "client" { - fmt.Println("Starting CertManager in client mode...") - // Client Task loop - go func() { - initClient() - defer wg.Done() - for { - select { - case <-ctx.Done(): - stopClient() - return - case <-reloadSigChan: - reloadClient() - case <-ticker.C: - clientTick() - } - } - }() - } else { - return fmt.Errorf("invalid operating mode \"" + config.GetString("App.mode") + "\"") - } - - // Cleanup on stop - sig := <-sigChannel - fmt.Printf("Program terminated with %v\n", sig.String()) - - stop() - wg.Wait() - return nil -} - -func stop() { - cancel() - clearPIDFile() -} - -func stopDaemon() error { - proc, err := getDaemonProcess() - if err != nil { - return fmt.Errorf("error getting daemon process: %v", err) - } - - err = proc.Signal(syscall.SIGTERM) - if err != nil { - return fmt.Errorf("error sending SIGTERM to daemon PID: %v", err) - } - return nil -} - -func reloadDaemon() error { - proc, err := getDaemonProcess() - if err != nil { - return fmt.Errorf("error getting daemon process: %v", err) - } - - err = proc.Signal(syscall.SIGHUP) - if err != nil { - return fmt.Errorf("error sending SIGHUP to daemon PID: %v", err) - } - return nil -} - -func tickDaemon() error { - proc, err := getDaemonProcess() - if err != nil { - return fmt.Errorf("error getting daemon process: %v", err) - } - - err = proc.Signal(syscall.SIGUSR1) - if err != nil { - return fmt.Errorf("error sending SIGUSR1 to daemon PID: %v", err) - } - return nil -} - -func statusDaemon() error { - fmt.Println("Not implemented :/") - return nil -} diff --git a/commands/basic.go b/commands/basic.go new file mode 100644 index 0000000..63c747a --- /dev/null +++ b/commands/basic.go @@ -0,0 +1,37 @@ +package commands + +import ( + "fmt" + "log" + + "git.nevets.tech/Keys/CertManager/internal" + "github.com/spf13/cobra" +) + +func DevCmd(cmd *cobra.Command, args []string) { + testDomain := "lunamc.org" + err := internal.LoadConfig() + if err != nil { + log.Fatalf("Error loading configuration: %v\n", err) + } + err = internal.LoadDomainConfigs() + if err != nil { + log.Fatalf("Error loading configs: %v\n", err) + } + + fmt.Println(testDomain) +} + +func VersionCmd(cmd *cobra.Command, args []string) { + fmt.Printf("CertManager (certman) - Steven Tracey\nVersion: %s build-%s\n", + internal.Version, internal.Build, + ) +} + +func NewKeyCmd(cmd *cobra.Command, args []string) { + key, err := internal.GenerateKey() + if err != nil { + log.Fatalf("%v", err) + } + fmt.Printf(key) +} diff --git a/commands/certs.go b/commands/certs.go new file mode 100644 index 0000000..9462352 --- /dev/null +++ b/commands/certs.go @@ -0,0 +1,266 @@ +package commands + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "git.nevets.tech/Keys/CertManager/internal" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5/storage/memory" +) + +var mgr *internal.ACMEManager + +func RenewCertCmd(domain string, noPush bool, certmanMode internal.CertManMode) error { + err := internal.LoadConfig() + if err != nil { + return err + } + err = internal.LoadDomainConfigs() + if err != nil { + return err + } + switch internal.Config().GetString("App.mode") { + case "server": + mgr, err = internal.NewACMEManager() + if err != nil { + return err + } + err = renewCerts(domain, noPush, certmanMode) + if err != nil { + return err + } + return ReloadDaemonCmd() + case "client": + return pullCerts(domain, certmanMode) + default: + return fmt.Errorf("invalid operating mode %s", internal.Config().GetString("App.mode")) + } +} + +func renewCerts(domain string, noPush bool, certmanMode internal.CertManMode) error { + _, err := mgr.RenewForDomain(domain) + if err != nil { + // if no existing cert, obtain instead + _, err = mgr.ObtainForDomain(domain) + if err != nil { + return fmt.Errorf("error obtaining domain certificates for domain %s: %v", domain, err) + } + } + + domainConfig, exists := internal.DomainStore().Get(domain) + if !exists { + return fmt.Errorf("domain %s does not exist", domain) + } + + domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix()) + err = internal.WriteDomainConfig(domainConfig) + if err != nil { + return fmt.Errorf("error saving domain config %s: %v", domain, err) + } + + err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), 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 = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), 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 := internal.CreateGiteaClient() + if giteaClient == nil { + return fmt.Errorf("error creating gitea client for domain %s: %v", domain, err) + } + gitWorkspace := &internal.GitWorkspace{ + Storage: memory.NewStorage(), + FS: memfs.New(), + } + + var repoUrl string + if !domainConfig.GetBool("Internal.repo_exists") { + repoUrl = internal.CreateGiteaRepo(domain, giteaClient) + if repoUrl == "" { + return fmt.Errorf("error creating Gitea repo for domain %s", domain) + } + domainConfig.Set("Internal.repo_exists", true) + err = internal.WriteDomainConfig(domainConfig) + if err != nil { + return fmt.Errorf("error saving domain config %s: %v", domain, err) + } + + err = internal.InitRepo(repoUrl, gitWorkspace) + if err != nil { + return fmt.Errorf("error initializing repo for domain %s: %v", domain, err) + } + } else { + repoUrl = internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" + err = internal.CloneRepo(repoUrl, gitWorkspace, certmanMode) + if err != nil { + return fmt.Errorf("error cloning repo for domain %s: %v", domain, err) + } + } + + err = internal.AddAndPushCerts(domain, gitWorkspace) + 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 +} + +func pullCerts(domain string, certmanMode internal.CertManMode) error { + gitWorkspace := &internal.GitWorkspace{ + Storage: memory.NewStorage(), + FS: memfs.New(), + } + + domainConfig, exists := internal.DomainStore().Get(domain) + if !exists { + return fmt.Errorf("domain %s does not exist", domain) + } + + // Ex: https://git.example.com/Org/Repo-suffix.git + // Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?) + repoUrl := internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git" + err := internal.CloneRepo(repoUrl, gitWorkspace, certmanMode) + if err != nil { + return fmt.Errorf("Error cloning domain repo %s: %v\n", domain, err) + } + + certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig) + if err != nil { + return fmt.Errorf("Error getting certificates dir for domain %s: %v\n", domain, err) + } + + // Get files in repo + fileInfos, err := gitWorkspace.FS.ReadDir("/") + if err != nil { + return fmt.Errorf("Error reading directory in memFS on domain %s: %v\n", domain, err) + } + // 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", domain, err) + continue + } + fileBytes, err := io.ReadAll(file) + if err != nil { + fmt.Printf("Error reading file in memFS on domain %s: %v\n", domain, err) + file.Close() + continue + } + err = file.Close() + if err != nil { + fmt.Printf("Error closing file on domain %s: %v\n", domain, err) + continue + } + + err = internal.DecryptFileFromBytes(domainConfig.GetString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) + if err != nil { + fmt.Printf("Error decrypting file %s in domain %s: %v\n", filename, domain, err) + continue + } + + headRef, err := gitWorkspace.Repo.Head() + if err != nil { + fmt.Printf("Error getting head reference for domain %s: %v\n", domain, err) + continue + } + + err = internal.WriteCommitHash(headRef.Hash().String(), domainConfig) + if err != nil { + fmt.Printf("Error writing commit hash: %v\n", err) + continue + } + + certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") + for _, certLink := range certLinks { + if certLink == "" { + continue + } + linkInfo, err := os.Stat(certLink) + if err != nil { + if !os.IsNotExist(err) { + fmt.Printf("Error stating cert link %s: %v\n", certLink, err) + continue + } + } + if linkInfo.IsDir() { + certLink = filepath.Join(certLink, domain+".crt") + } + + err = os.Link(filepath.Join(certsDir, domain+".crt"), certLink) + if err != nil { + fmt.Printf("Error linking cert %s to %s: %v\n", certLink, domain, err) + continue + } + } + + keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") + for _, keyLink := range keyLinks { + if keyLink == "" { + continue + } + linkInfo, err := os.Stat(keyLink) + if err != nil { + if !os.IsNotExist(err) { + fmt.Printf("Error stating key link %s: %v\n", keyLink, err) + continue + } + } + if linkInfo.IsDir() { + keyLink = filepath.Join(keyLink, domain+".crt") + } + + err = os.Link(filepath.Join(certsDir, domain+".crt"), keyLink) + if err != nil { + fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domain, err) + continue + } + } + } + } + return nil +} + +func UpdateLinksCmd(domain string) error { + domainConfig, exists := internal.DomainStore().Get(domain) + if !exists { + return fmt.Errorf("domain %s does not exist", domain) + } + + certsDir, err := internal.DomainCertsDirWConf(domain, domainConfig) + if err != nil { + return fmt.Errorf("error getting certificates dir for domain %s: %v", domain, err) + } + + certLinks := domainConfig.GetStringSlice("Certificates.cert_symlinks") + for _, certLink := range certLinks { + err = internal.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.GetStringSlice("Certificates.key_symlinks") + for _, keyLink := range keyLinks { + err = internal.LinkFile(filepath.Join(certsDir, domain+".crt"), keyLink, domain, ".key") + if err != nil { + fmt.Printf("Error linking cert %s to %s: %v", keyLink, domain, err) + continue + } + } + return nil +} diff --git a/commands/daemon.go b/commands/daemon.go new file mode 100644 index 0000000..e23cae7 --- /dev/null +++ b/commands/daemon.go @@ -0,0 +1,164 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "git.nevets.tech/Keys/CertManager/client" + "git.nevets.tech/Keys/CertManager/internal" + "git.nevets.tech/Keys/CertManager/server" +) + +var ( + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +) + +func RunDaemonCmd() error { + err := internal.CreateOrUpdatePIDFile("/var/run/certman.pid") + if err != nil { + if errors.Is(err, internal.ErrorPIDInUse) { + return fmt.Errorf("daemon process is already running") + } + return fmt.Errorf("error creating pidfile: %v", err) + } + + ctx, cancel = context.WithCancel(context.Background()) + + // Check if main config exists + if _, err := os.Stat("/etc/certman/certman.conf"); os.IsNotExist(err) { + return fmt.Errorf("main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf") + } else if err != nil { + return fmt.Errorf("error opening /etc/certman/certman.conf: %v", err) + } + err = internal.LoadConfig() + if err != nil { + return fmt.Errorf("error loading configuration: %v", err) + } + + // Setup SIGINT and SIGTERM listeners + sigChannel := make(chan os.Signal, 1) + signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM) + defer signal.Stop(sigChannel) + + reloadSigChan := make(chan os.Signal, 1) + signal.Notify(reloadSigChan, syscall.SIGHUP) + defer signal.Stop(reloadSigChan) + + tickSigChan := make(chan os.Signal, 1) + signal.Notify(tickSigChan, syscall.SIGUSR1) + defer signal.Stop(tickSigChan) + + tickRate := internal.Config().GetInt("App.tick_rate") + ticker := time.NewTicker(time.Duration(tickRate) * time.Hour) + defer ticker.Stop() + + wg.Add(1) + if internal.Config().GetString("App.mode") == "server" { + fmt.Println("Starting CertManager in server mode...") + // Server Task loop + go func() { + server.Init() + defer wg.Done() + for { + select { + case <-ctx.Done(): + server.Stop() + return + case <-reloadSigChan: + server.Reload() + case <-ticker.C: + server.Tick() + case <-tickSigChan: + server.Tick() + } + } + }() + } else if internal.Config().GetString("App.mode") == "client" { + fmt.Println("Starting CertManager in client mode...") + // Client Task loop + go func() { + client.Init() + defer wg.Done() + for { + select { + case <-ctx.Done(): + client.Stop() + return + case <-reloadSigChan: + client.Reload() + case <-ticker.C: + client.Tick() + case <-tickSigChan: + client.Tick() + } + } + }() + } else { + return fmt.Errorf("invalid operating mode \"" + internal.Config().GetString("App.mode") + "\"") + } + + // Cleanup on stop + sig := <-sigChannel + fmt.Printf("Program terminated with %v\n", sig.String()) + + stop() + wg.Wait() + return nil +} + +func stop() { + cancel() + internal.ClearPIDFile() +} + +func StopDaemonCmd() error { + proc, err := internal.DaemonProcess() + if err != nil { + return fmt.Errorf("error getting daemon process: %v", err) + } + + err = proc.Signal(syscall.SIGTERM) + if err != nil { + return fmt.Errorf("error sending SIGTERM to daemon PID: %v", err) + } + return nil +} + +func ReloadDaemonCmd() error { + proc, err := internal.DaemonProcess() + if err != nil { + return fmt.Errorf("error getting daemon process: %v", err) + } + + err = proc.Signal(syscall.SIGHUP) + if err != nil { + return fmt.Errorf("error sending SIGHUP to daemon PID: %v", err) + } + return nil +} + +func TickDaemonCmd() error { + proc, err := internal.DaemonProcess() + if err != nil { + return fmt.Errorf("error getting daemon process: %v", err) + } + + err = proc.Signal(syscall.SIGUSR1) + if err != nil { + return fmt.Errorf("error sending SIGUSR1 to daemon PID: %v", err) + } + return nil +} + +func DaemonStatusCmd() error { + fmt.Println("Not implemented :/") + return nil +} diff --git a/commands/install.go b/commands/install.go new file mode 100644 index 0000000..69f4c62 --- /dev/null +++ b/commands/install.go @@ -0,0 +1,98 @@ +package commands + +import ( + "fmt" + "os" + "os/exec" + "os/user" + "strconv" + "strings" + + "git.nevets.tech/Keys/CertManager/internal" +) + +func NewDomainCmd(domain, domainDir string, dirOverridden bool) error { + //TODO add config option for "overriden dir" + fmt.Printf("Creating new domain %s\n", domain) + err := internal.CreateDomainConfig(domain) + if err != nil { + return err + } + internal.CreateDomainCertsDir(domain, domainDir, dirOverridden) + + certmanUser, err := user.Lookup("certman") + if err != nil { + return fmt.Errorf("error getting user certman: %v", err) + } + uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid)) + if err != nil { + return err + } + gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid)) + if err != nil { + return err + } + err = internal.ChownRecursive("/etc/certman/domains", uid, gid) + if err != nil { + return err + } + err = internal.ChownRecursive("/var/local/certman", uid, gid) + if err != nil { + return err + } + + fmt.Println("Successfully created domain entry for " + domain + "\nUpdate config file as needed in /etc/certman/domains/" + domain + ".conf\n") + return nil +} + +func InstallCmd(isThin bool, mode string) error { + if !isThin { + if os.Geteuid() != 0 { + return fmt.Errorf("installation must be run as root") + } + + internal.MakeDirs() + internal.CreateConfig(mode) + + f, err := os.OpenFile("/var/run/certman.pid", os.O_RDONLY|os.O_CREATE, 0755) + if err != nil { + return fmt.Errorf("error creating pid file: %v", err) + } + err = f.Close() + if err != nil { + return fmt.Errorf("error closing pid file: %v", err) + } + + newUserCmd := exec.Command("useradd", "-d", "/var/local/certman", "-U", "-r", "-s", "/sbin/nologin", "certman") + if output, err := newUserCmd.CombinedOutput(); err != nil { + return fmt.Errorf("error creating user: %v: output %s", err, output) + } + certmanUser, err := user.Lookup("certman") + if err != nil { + return fmt.Errorf("error getting user certman: %v", err) + } + uid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Uid)) + if err != nil { + return err + } + gid, err := strconv.Atoi(strings.TrimSpace(certmanUser.Gid)) + if err != nil { + return err + } + err = internal.ChownRecursive("/etc/certman", uid, gid) + if err != nil { + return fmt.Errorf("error changing uid/gid: %v", err) + } + err = internal.ChownRecursive("/var/local/certman", uid, gid) + if err != nil { + return fmt.Errorf("error changing uid/gid: %v", err) + } + err = os.Chown("/var/run/certman.pid", uid, gid) + if err != nil { + return fmt.Errorf("error changing uid/gid: %v", err) + } + } else { + internal.CreateConfig(mode) + } + return nil +} diff --git a/example.config.conf b/example.config.conf deleted file mode 100644 index 369a768..0000000 --- a/example.config.conf +++ /dev/null @@ -1,17 +0,0 @@ -[App] -mode = {mode} - -[Git] -host = gitea -server = https://gitea.instance.com -username = user -org_name = org -template_name = template - -[Certificates] -email = user@example.com -data_root = /var/local/certman -request_method = dns - -[Cloudflare] -cf_email = email@example.com diff --git a/example.domainconfig.conf b/example.domainconfig.conf deleted file mode 100644 index d172160..0000000 --- a/example.domainconfig.conf +++ /dev/null @@ -1,19 +0,0 @@ -[Domain] -domain_name = {domain} -; default (use system dns) or IPv4 Address (1.1.1.1) -dns_server = default -; optionally use /path/to/directory -file_location = default - -[Certificates] -subdomains = -expiry = 90 -cert_symlink = -key_symlink = - -[Repo] -repo_suffix = -certificates - -; Don't change setting below here unless you know what you're doing! -[Internal] -last_issued = never \ No newline at end of file diff --git a/go.mod b/go.mod index b802774..30139fb 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module main +module git.nevets.tech/Keys/CertManager go 1.25.0 @@ -7,7 +7,6 @@ require ( github.com/go-acme/lego/v4 v4.32.0 github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.17.0 - github.com/google/go-github/v55 v55.0.0 github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 @@ -30,7 +29,6 @@ require ( github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/go-querystring v1.2.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect diff --git a/acme_manager.go b/internal/acme_manager.go similarity index 92% rename from acme_manager.go rename to internal/acme_manager.go index 349bc12..0e8740e 100644 --- a/acme_manager.go +++ b/internal/acme_manager.go @@ -1,5 +1,4 @@ -// acme_manager.go -package main +package internal import ( "crypto" @@ -28,14 +27,14 @@ import ( // Thread safety for your domain config map // (assumes you already have these globals elsewhere) // --------------------------------------------- -// var mu sync.RWMutex +// var MU sync.RWMutex // var domainConfigs map[string]*ezconf.Configuration // var config *ezconf.Configuration // // func getDomainConfig(domain string) (*ezconf.Configuration, bool) { ... } // --------------------------------------------- -// ACME account user (file-backed) +// ACME account User (file-backed) // --------------------------------------------- type fileUser struct { @@ -54,14 +53,14 @@ func (u *fileUser) GetPrivateKey() crypto.PrivateKey { return u.privateKe // --------------------------------------------- type ACMEManager struct { - mu sync.Mutex // serializes lego client ops + account writes - client *lego.Client - user *fileUser + MU sync.Mutex // serializes lego Client ops + account writes + Client *lego.Client + User *fileUser // root dirs dataRoot string // e.g. /var/local/certman accountRoot string // e.g. /var/local/certman/accounts - certsRoot string // e.g. /var/local/certman/certificates + CertsRoot string // e.g. /var/local/certman/certificates } // DomainRuntimeConfig has domain-specific runtime settings derived from main+domain config. @@ -89,7 +88,7 @@ type StoredCertMeta struct { // Public API // --------------------------------------------- -// NewACMEManager initializes a long-lived lego client using: +// NewACMEManager initializes a long-lived lego Client using: // - file-backed account // - persistent ECDSA P-256 account key // - Let’s Encrypt production by default (from config fallback) @@ -104,20 +103,20 @@ func NewACMEManager() (*ACMEManager, error) { mgr := &ACMEManager{ dataRoot: dataRoot, accountRoot: filepath.Join(dataRoot, "accounts"), - certsRoot: filepath.Join(dataRoot, "certificates"), + CertsRoot: filepath.Join(dataRoot, "certificates"), } if err := os.MkdirAll(mgr.accountRoot, 0o700); err != nil { return nil, fmt.Errorf("create account root: %w", err) } - if err := os.MkdirAll(mgr.certsRoot, 0o700); err != nil { + if err := os.MkdirAll(mgr.CertsRoot, 0o700); err != nil { return nil, fmt.Errorf("create certs root: %w", err) } - // Create/load file-backed account user + // Create/load file-backed account User user, err := loadOrCreateACMEUser(mgr.accountRoot, email) if err != nil { - return nil, fmt.Errorf("load/create acme user: %w", err) + return nil, fmt.Errorf("load/create acme User: %w", err) } // Cloudflare provider (DNS-01 only). @@ -147,20 +146,20 @@ func NewACMEManager() (*ACMEManager, error) { return nil, fmt.Errorf("set dns-01 provider: %w", err) } - mgr.client = client - mgr.user = user + mgr.Client = client + mgr.User = user // Register account only on first run - if mgr.user.Registration == nil { - reg, err := mgr.client.Registration.Register(registration.RegisterOptions{ + if mgr.User.Registration == nil { + reg, err := mgr.Client.Registration.Register(registration.RegisterOptions{ TermsOfServiceAgreed: true, }) if err != nil { return nil, fmt.Errorf("acme registration: %w", err) } - mgr.user.Registration = reg - if err := saveACMEUser(mgr.accountRoot, mgr.user); err != nil { - return nil, fmt.Errorf("save acme user registration: %w", err) + mgr.User.Registration = reg + if err := saveACMEUser(mgr.accountRoot, mgr.User); err != nil { + return nil, fmt.Errorf("save acme User registration: %w", err) } } @@ -184,10 +183,10 @@ func (m *ACMEManager) ObtainForDomain(domainKey string) (*certificate.Resource, Bundle: true, } - m.mu.Lock() - defer m.mu.Unlock() + m.MU.Lock() + defer m.MU.Unlock() - res, err := m.client.Certificate.Obtain(req) + res, err := m.Client.Certificate.Obtain(req) if err != nil { return nil, fmt.Errorf("obtain %q: %w", domainKey, err) } @@ -201,8 +200,8 @@ func (m *ACMEManager) ObtainForDomain(domainKey string) (*certificate.Resource, // RenewForDomain renews an existing stored cert for a domain key. func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, error) { - m.mu.Lock() - defer m.mu.Unlock() + m.MU.Lock() + defer m.MU.Unlock() existing, err := m.loadStoredResource(domainKey) if err != nil { @@ -210,7 +209,7 @@ func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, e } // RenewWithOptions is preferred in newer lego versions. - renewed, err := m.client.Certificate.RenewWithOptions(*existing, &certificate.RenewOptions{ + renewed, err := m.Client.Certificate.RenewWithOptions(*existing, &certificate.RenewOptions{ Bundle: true, }) if err != nil { @@ -226,8 +225,8 @@ func (m *ACMEManager) RenewForDomain(domainKey string) (*certificate.Resource, e // GetCertPaths returns disk paths for the domain's cert material. func (m *ACMEManager) GetCertPaths(domainKey string) (certPEM, keyPEM string) { - base := sanitizeDomainKey(domainKey) - dir := filepath.Join(m.certsRoot, base) + base := SanitizeDomainKey(domainKey) + dir := filepath.Join(m.CertsRoot, base) return filepath.Join(dir, base+".crt"), filepath.Join(dir, base+".key") } @@ -450,7 +449,7 @@ func loadACMEUser(accountRoot string) (*fileUser, error) { func saveACMEUser(accountRoot string, u *fileUser) error { if u == nil { - return errors.New("nil user") + return errors.New("nil User") } if err := os.MkdirAll(accountRoot, 0o700); err != nil { return err @@ -525,8 +524,8 @@ func (m *ACMEManager) saveCertFiles(domainKey string, res *certificate.Resource, return errors.New("nil certificate resource") } - base := sanitizeDomainKey(domainKey) - dir := filepath.Join(m.certsRoot, base) + base := SanitizeDomainKey(domainKey) + dir := filepath.Join(m.CertsRoot, base) if err := os.MkdirAll(dir, 0o700); err != nil { return err } @@ -599,8 +598,8 @@ func (m *ACMEManager) saveCertFiles(domainKey string, res *certificate.Resource, } func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resource, error) { - base := sanitizeDomainKey(domainKey) - dir := filepath.Join(m.certsRoot, base) + base := SanitizeDomainKey(domainKey) + dir := filepath.Join(m.CertsRoot, base) raw, err := os.ReadFile(filepath.Join(dir, base+".json")) if err != nil { return nil, err @@ -623,8 +622,8 @@ func (m *ACMEManager) loadStoredResource(domainKey string) (*certificate.Resourc } func (m *ACMEManager) loadMeta(domainKey string) (*StoredCertMeta, error) { - base := sanitizeDomainKey(domainKey) - dir := filepath.Join(m.certsRoot, base) + base := SanitizeDomainKey(domainKey) + dir := filepath.Join(m.CertsRoot, base) raw, err := os.ReadFile(filepath.Join(dir, base+".meta.json")) if err != nil { return nil, err diff --git a/internal/buildinfo.go b/internal/buildinfo.go new file mode 100644 index 0000000..cd7b8a0 --- /dev/null +++ b/internal/buildinfo.go @@ -0,0 +1,6 @@ +package internal + +var ( + Version = "dev" + Build = "local" +) diff --git a/config.go b/internal/config.go similarity index 75% rename from config.go rename to internal/config.go index 2048c92..94827ea 100644 --- a/config.go +++ b/internal/config.go @@ -1,4 +1,4 @@ -package main +package internal import ( "bytes" @@ -67,19 +67,48 @@ func (s *DomainConfigStore) Snapshot() map[string]*viper.Viper { var ( config *viper.Viper + configMu sync.RWMutex domainStore = NewDomainConfigStore() ) +func Config() *viper.Viper { + configMu.RLock() + defer configMu.RUnlock() + return config +} + +func DomainStore() *DomainConfigStore { + domainStore.mu.RLock() + defer domainStore.mu.RUnlock() + return domainStore +} + // --------------------------------------------------------------------------- // Loading // --------------------------------------------------------------------------- // LoadConfig reads the main certman.conf into config. -func LoadConfig(path string) error { +func LoadConfig() error { config = viper.New() - config.SetConfigFile(path) + config.SetConfigFile("/etc/certman/certman.conf") config.SetConfigType("toml") - return config.ReadInConfig() + err := config.ReadInConfig() + if err != nil { + return err + } + + switch config.GetString("App.mode") { + case "server": + config.SetConfigType("toml") + config.SetConfigFile("server.conf") + return config.MergeInConfig() + case "Client": + config.SetConfigType("toml") + config.SetConfigFile("Client.conf") + return config.MergeInConfig() + } + + return nil } // LoadDomainConfigs reads every .conf file in the domains directory. @@ -223,7 +252,7 @@ func EffectiveBool(domainCfg *viper.Viper, key string) bool { // Directory bootstrapping // --------------------------------------------------------------------------- -func makeDirs() { +func MakeDirs() { dirs := []struct { path string perm os.FileMode @@ -240,31 +269,49 @@ func makeDirs() { } } -func createNewConfig(mode string) { +func CreateConfig(mode string) { content := strings.NewReplacer( "{mode}", mode, - "{uuid}", uuid.New().String(), ).Replace(defaultConfig) createFile("/etc/certman/certman.conf", 0640, []byte(content)) + + switch mode { + case "server": + content = strings.NewReplacer( + "{uuid}", uuid.New().String(), + ).Replace(defaultServerConfig) + createFile("/etc/certman/server.conf", 640, []byte(content)) + } } -func createNewDomainConfig(domain string) error { +func CreateDomainConfig(domain string) error { key, err := GenerateKey() if err != nil { return fmt.Errorf("unable to generate key: %v", err) } - content := strings.NewReplacer( - "{domain}", domain, - "{key}", key, - ).Replace(defaultDomainConfig) + var content string + switch Config().GetString("App.mode") { + case "server": + content = strings.NewReplacer( + "{domain}", domain, + "{key}", key, + ).Replace(defaultServerDomainConfig) + case "Client": + content = strings.NewReplacer( + "{domain}", domain, + "{key}", key, + ).Replace(defaultClientDomainConfig) + default: + return fmt.Errorf("unknown certman mode: %v", Config().GetString("App.mode")) + } path := filepath.Join("/etc/certman/domains", domain+".conf") createFile(path, 0640, []byte(content)) return nil } -func createNewDomainCertsDir(domain string, dir string, dirOverride bool) { +func CreateDomainCertsDir(domain string, dir string, dirOverride bool) { var target string if dirOverride { target = filepath.Join(dir, domain) @@ -286,49 +333,68 @@ func createNewDomainCertsDir(domain string, dir string, dirOverride bool) { // --------------------------------------------------------------------------- const defaultConfig = `[App] -mode = "{mode}" +mode = '{mode}' tick_rate = 2 -uuid = "{uuid}" [Git] -host = "gitea" -server = "https://gitea.instance.com" -username = "user" -api_token = "xxxxxxxxxxxxxxxxxxxxxxxxx" -org_name = "org" +host = 'gitea' +server = 'https://gitea.instance.com' +username = 'User' +api_token = 'xxxxxxxxxxxxxxxxxxxxxxxxx' +org_name = 'org' [Certificates] -email = "user@example.com" -data_root = "/var/local/certman" -ca_dir_url = "https://acme-v02.api.letsencrypt.org/directory" - -[Cloudflare] -cf_email = "email@example.com" -cf_api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx" +data_root = '/var/local/certman' ` -const defaultDomainConfig = `[Domain] -domain_name = "{domain}" -enabled = true -dns_server = "default" +const defaultServerConfig = `[App] +uuid = '{uuid}' [Certificates] -data_root = "" +email = 'User@example.com' +data_root = '/var/local/certman' +ca_dir_url = 'https://acme-v02.api.letsencrypt.org/directory' + +[Cloudflare] +cf_email = 'email@example.com' +cf_api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx'` + +const defaultClientConfig = `` + +const defaultServerDomainConfig = `[Domain] +domain_name = '{domain}' +enabled = true +dns_server = 'default' + +[Certificates] +data_root = '' expiry = 90 -request_method = "dns-01" +request_method = 'dns-01' renew_period = 30 subdomains = [] -cert_symlinks = [] -key_symlinks = [] -crypto_key = "{key}" +crypto_key = '{key}' [Repo] -repo_suffix = "-certificates" +repo_suffix = '-certificates' [Internal] last_issued = 0 repo_exists = false -status = "clean" +status = 'clean' +` + +const defaultClientDomainConfig = `[Certificates] +data_root = '' +cert_symlinks = [] +key_symlinks = [] +crypto_key = '{key}' + +[Domain] +domain_name = '{domain}' +enabled = true + +[Repo] +repo_suffix = '-certificates' ` const readme = `` diff --git a/crypto.go b/internal/crypto.go similarity index 99% rename from crypto.go rename to internal/crypto.go index 3bfd532..2f3965a 100644 --- a/crypto.go +++ b/internal/crypto.go @@ -1,4 +1,4 @@ -package main +package internal import ( "crypto/rand" diff --git a/git.go b/internal/git.go similarity index 75% rename from git.go rename to internal/git.go index 30b1fb8..5f4a438 100644 --- a/git.go +++ b/internal/git.go @@ -1,7 +1,6 @@ -package main +package internal import ( - "context" "errors" "fmt" "io" @@ -17,10 +16,16 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" - "github.com/google/go-github/v55/github" "github.com/spf13/viper" ) +type CertManMode int + +const ( + Server CertManMode = iota + Client +) + type GitWorkspace struct { Repo *git.Repository Storage *memory.Storage @@ -48,7 +53,7 @@ var GitSourceName = map[GitSource]string{ CodeCommit: "code-commit", } -func strToGitSource(s string) (GitSource, error) { +func StrToGitSource(s string) (GitSource, error) { for k, v := range GitSourceName { if v == s { return k, nil @@ -57,11 +62,11 @@ func strToGitSource(s string) (GitSource, error) { return GitSource(0), errors.New("invalid gitsource name") } -func createGithubClient() *github.Client { - return github.NewClient(nil).WithAuthToken(config.GetString("Git.api_token")) -} +//func createGithubClient() *github.Client { +// return github.NewClient(nil).WithAuthToken(config.GetString("Git.api_token")) +//} -func createGiteaClient() *gitea.Client { +func CreateGiteaClient() *gitea.Client { client, err := gitea.NewClient(config.GetString("Git.server"), gitea.SetToken(config.GetString("Git.api_token"))) if err != nil { fmt.Printf("Error connecting to gitea instance: %v\n", err) @@ -70,30 +75,30 @@ func createGiteaClient() *gitea.Client { return client } -func createGithubRepo(domain *Domain, client *github.Client) string { - name := domain.name - owner := domain.config.GetString("Repo.owner") - description := domain.description - private := true - includeAllBranches := false +//func createGithubRepo(domain *Domain, Client *github.Client) string { +// name := domain.name +// owner := domain.config.GetString("Repo.owner") +// description := domain.description +// private := true +// includeAllBranches := false +// +// ctx := context.Background() +// template := &github.TemplateRepoRequest{ +// Name: name, +// Owner: &owner, +// Description: description, +// Private: &private, +// IncludeAllBranches: &includeAllBranches, +// } +// repo, _, err := Client.Repositories.CreateFromTemplate(ctx, config.GetString("Git.org_name"), config.GetString("Git.template_name"), template) +// if err != nil { +// fmt.Println("Error creating repository from template,", err) +// return "" +// } +// return *repo.CloneURL +//} - ctx := context.Background() - template := &github.TemplateRepoRequest{ - Name: name, - Owner: &owner, - Description: description, - Private: &private, - IncludeAllBranches: &includeAllBranches, - } - repo, _, err := client.Repositories.CreateFromTemplate(ctx, config.GetString("Git.org_name"), config.GetString("Git.template_name"), template) - if err != nil { - fmt.Println("Error creating repository from template,", err) - return "" - } - return *repo.CloneURL -} - -func createGiteaRepo(domain string, giteaClient *gitea.Client) string { +func CreateGiteaRepo(domain string, giteaClient *gitea.Client) string { domainConfig, exists := domainStore.Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) @@ -121,7 +126,7 @@ func createGiteaRepo(domain string, giteaClient *gitea.Client) string { return giteaRepo.CloneURL } -func initRepo(url string, ws *GitWorkspace) error { +func InitRepo(url string, ws *GitWorkspace) error { var err error ws.Repo, err = git.Init(ws.Storage, ws.FS) if err != nil { @@ -147,7 +152,7 @@ func initRepo(url string, ws *GitWorkspace) error { return nil } -func cloneRepo(url string, ws *GitWorkspace) error { +func CloneRepo(url string, ws *GitWorkspace, certmanMode CertManMode) error { creds := &http.BasicAuth{ Username: config.GetString("Git.username"), Password: config.GetString("Git.api_token"), @@ -163,33 +168,35 @@ func cloneRepo(url string, ws *GitWorkspace) error { fmt.Printf("Error getting worktree from cloned repo: %v\n", err) return err } - serverIdFile, err := ws.FS.OpenFile("/SERVER_ID", os.O_RDWR, 0640) - if err != nil { - if os.IsNotExist(err) { - fmt.Printf("Server ID file not found for %s, adopting domain\n", url) - return nil + if certmanMode == Server { + serverIdFile, err := ws.FS.OpenFile("/SERVER_ID", os.O_RDWR, 0640) + if err != nil { + if os.IsNotExist(err) { + fmt.Printf("Server ID file not found for %s, adopting domain\n", url) + return nil + } + return err + } + serverIdBytes, err := io.ReadAll(serverIdFile) + if err != nil { + return err + } + serverId := strings.TrimSpace(string(serverIdBytes)) + if serverId != config.GetString("App.uuid") { + return fmt.Errorf("domain is already managed by server with uuid %s", serverId) } - return err - } - serverIdBytes, err := io.ReadAll(serverIdFile) - if err != nil { - return err - } - serverId := strings.TrimSpace(string(serverIdBytes)) - if serverId != config.GetString("App.uuid") { - return fmt.Errorf("domain is already managed by server with uuid %s", serverId) } return nil } -func addAndPushCerts(domain string, ws *GitWorkspace) error { +func AddAndPushCerts(domain string, ws *GitWorkspace) error { domainConfig, exists := domainStore.Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) return ErrConfigNotFound } - certsDir, err := getDomainCertsDirWConf(domain, domainConfig) + certsDir, err := DomainCertsDirWConf(domain, domainConfig) if err != nil { if errors.Is(err, ErrConfigNotFound) { fmt.Printf("Domain %s config not found: %v\n", domain, err) @@ -301,8 +308,8 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { return nil } -func writeCommitHash(hash string, domainConfig *viper.Viper) error { - certsDir, err := getDomainCertsDirWOnlyConf(domainConfig) +func WriteCommitHash(hash string, domainConfig *viper.Viper) error { + certsDir, err := DomainCertsDirWOnlyConf(domainConfig) if err != nil { if errors.Is(err, ErrConfigNotFound) { return err @@ -318,8 +325,8 @@ func writeCommitHash(hash string, domainConfig *viper.Viper) error { return nil } -func getLocalCommitHash(domain string) (string, error) { - certsDir, err := getDomainCertsDir(domain) +func LocalCommitHash(domain string) (string, error) { + certsDir, err := DomainCertsDir(domain) if err != nil { if errors.Is(err, ErrConfigNotFound) { fmt.Printf("Domain %s config not found: %v\n", domain, err) @@ -330,15 +337,17 @@ func getLocalCommitHash(domain string) (string, error) { data, err := os.ReadFile(filepath.Join(certsDir, "hash")) if err != nil { - fmt.Printf("Error reading file for domain %s: %v\n", domain, err) - return "", err + if !os.IsNotExist(err) { + fmt.Printf("Error reading file for domain %s: %v\n", domain, err) + return "", err + } } return strings.TrimSpace(string(data)), nil } -func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) { - domainConfig, exists := domainStore.Get(domain) +func RemoteCommitHash(domain string, gitSource GitSource) (string, error) { + domainConfig, exists := DomainStore().Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) return "", ErrConfigNotFound @@ -354,7 +363,7 @@ func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) { } func getRemoteCommitHashGitea(org, repo, branchName string) (string, error) { - giteaClient := createGiteaClient() + giteaClient := CreateGiteaClient() branch, _, err := giteaClient.GetRepoBranch(org, repo, branchName) if err != nil { fmt.Printf("Error getting repo branch: %v\n", err) diff --git a/util.go b/internal/util.go similarity index 88% rename from util.go rename to internal/util.go index b7d2b2c..df72fa9 100644 --- a/util.go +++ b/internal/util.go @@ -1,4 +1,4 @@ -package main +package internal import ( "errors" @@ -11,7 +11,6 @@ import ( "syscall" "code.gitea.io/sdk/gitea" - "github.com/google/go-github/v55/github" "github.com/spf13/viper" ) @@ -25,7 +24,6 @@ type Domain struct { name *string config *viper.Viper description *string - ghClient *github.Client gtClient *gitea.Client } @@ -44,7 +42,7 @@ func createPIDFile() { } // 0x02 -func clearPIDFile() { +func ClearPIDFile() { file, err := os.OpenFile("/var/run/certman.pid", os.O_RDWR|os.O_CREATE, 0644) if err != nil { fmt.Printf("0x02: Error opening PID file: %v\n", err) @@ -60,7 +58,7 @@ func clearPIDFile() { } // 0x03 -func createOrUpdatePIDFile(filename string) error { +func CreateOrUpdatePIDFile(filename string) error { pidBytes, err := os.ReadFile(filename) if err != nil { fmt.Printf("0x03: Error reading PID file: %v\n", err) @@ -142,7 +140,7 @@ func isProcessActive(pid int) (bool, error) { } // 0x05 -func getDaemonProcess() (*os.Process, error) { +func DaemonProcess() (*os.Process, error) { pidBytes, err := os.ReadFile("/var/run/certman.pid") if err != nil { fmt.Printf("0x05: Error getting PID from /var/run/certman.pid: %v\n", err) @@ -209,15 +207,20 @@ func createFile(fileName string, filePermission os.FileMode, data []byte) { } } -func linkFile(source, target, domain, extension string) error { +func LinkFile(source, target, domain, extension string) error { if target == "" { return ErrBlankCert } linkInfo, err := os.Stat(target) if err != nil { - if !os.IsNotExist(err) { - return err + if os.IsNotExist(err) { + err = os.Symlink(source, target) + if err != nil { + return err + } + return nil } + return err } if linkInfo.IsDir() { target = filepath.Join(target, domain+extension) @@ -252,24 +255,24 @@ func insert(a []string, index int, value string) []string { return a } -func sanitizeDomainKey(s string) string { +func SanitizeDomainKey(s string) string { s = strings.TrimSpace(strings.ToLower(s)) r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_") return r.Replace(s) } -// getDomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors -func getDomainCertsDir(domain string) (string, error) { +// DomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors +func DomainCertsDir(domain string) (string, error) { domainConfig, exists := domainStore.Get(domain) if !exists { return "", ErrConfigNotFound } - return getDomainCertsDirWConf(domain, domainConfig) + return DomainCertsDirWConf(domain, domainConfig) } -// getDomainCertsDir Can return ErrBlankConfigEntry or other errors -func getDomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) { +// DomainCertsDirWConf Can return ErrBlankConfigEntry or other errors +func DomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) { effectiveDataRoot, err := EffectiveString(domainConfig, "Certificates.data_root") if err != nil { return "", err @@ -278,9 +281,9 @@ func getDomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, e return filepath.Join(effectiveDataRoot, "certificates", domain), nil } -func getDomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) { +func DomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) { domain := domainConfig.GetString("Domain.domain_name") - return getDomainCertsDirWConf(domain, domainConfig) + return DomainCertsDirWConf(domain, domainConfig) } func ChownRecursive(path string, uid, gid int) error { diff --git a/main.go b/main.go index 4f1b7e9..5b1b175 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,16 @@ package main import ( - "context" "fmt" "os" "regexp" - "sync" + "git.nevets.tech/Keys/CertManager/commands" + "git.nevets.tech/Keys/CertManager/internal" "github.com/spf13/cobra" ) -var version = "1.0.0" -var build = "1" - -var ( - configFile string - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup -) +var configFile string var fqdnRegex = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`) @@ -36,9 +28,9 @@ func main() { rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "/etc/certman/certman.conf", "Configuration file") - rootCmd.AddCommand(basicCmd("version", "Show version", versionResponse)) - rootCmd.AddCommand(basicCmd("gen-key", "Generates encryption key", newKey)) - rootCmd.AddCommand(basicCmd("dev", "Dev Function", devFunc)) + rootCmd.AddCommand(basicCmd("version", "Show version", commands.VersionCmd)) + rootCmd.AddCommand(basicCmd("gen-key", "Generates encryption key", commands.NewKeyCmd)) + rootCmd.AddCommand(basicCmd("dev", "Dev Function", commands.DevCmd)) var domainCertDir string newDomainCmd := &cobra.Command{ @@ -49,7 +41,7 @@ func main() { SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { dirOverridden := cmd.Flags().Changed("dir") - return newDomain(args[0], domainCertDir, dirOverridden) + return commands.NewDomainCmd(args[0], domainCertDir, dirOverridden) }, } newDomainCmd.Flags().StringVar(&domainCertDir, "dir", "/var/local/certman/certificates/", "Alternate directory for certificates") @@ -65,7 +57,7 @@ func main() { RunE: func(cmd *cobra.Command, args []string) error { switch modeFlag { case "server", "client": - return install(thinInstallFlag, modeFlag) + return commands.InstallCmd(thinInstallFlag, modeFlag) default: return fmt.Errorf("invalid --mode %q (must be server or client)", modeFlag) } @@ -89,12 +81,22 @@ func main() { Short: "Renews a domains certificate", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return renewCertFunc(args[0], noPush) + return commands.RenewCertCmd(args[0], noPush, internal.Server) }, } renewCertCmd.Flags().BoolVar(&noPush, "no-push", false, "Don't push certs to repo, renew locally only [server mode only]") certCmd.AddCommand(renewCertCmd) + updateCertLinkCmd := &cobra.Command{ + Use: "update-link", + Short: "Update linked certificates", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return commands.UpdateLinksCmd(args[0]) + }, + } + certCmd.AddCommand(updateCertLinkCmd) + rootCmd.AddCommand(certCmd) daemonCmd := &cobra.Command{ @@ -110,7 +112,7 @@ func main() { Short: "Start the daemon", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runDaemon() + return commands.RunDaemonCmd() }, }) @@ -119,7 +121,7 @@ func main() { Short: "Stop the daemon", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return stopDaemon() + return commands.StopDaemonCmd() }, }) @@ -128,7 +130,7 @@ func main() { Short: "Reload daemon configs", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return reloadDaemon() + return commands.ReloadDaemonCmd() }, }) @@ -137,7 +139,7 @@ func main() { Short: "Manually triggers daemon tick", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return tickDaemon() + return commands.TickDaemonCmd() }, }) @@ -146,7 +148,7 @@ func main() { Short: "Show daemon status", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return statusDaemon() + return commands.DaemonStatusCmd() }, }) diff --git a/server.go b/server/server.go similarity index 66% rename from server.go rename to server/server.go index e6353dc..b24b548 100644 --- a/server.go +++ b/server/server.go @@ -1,4 +1,4 @@ -package main +package server import ( "fmt" @@ -7,23 +7,24 @@ import ( "sync" "time" + "git.nevets.tech/Keys/CertManager/internal" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5/storage/memory" ) var ( tickMu sync.Mutex - mgr *ACMEManager + mgr *internal.ACMEManager mgrMu sync.Mutex ) -func getACMEManager() (*ACMEManager, error) { +func getACMEManager() (*internal.ACMEManager, error) { mgrMu.Lock() defer mgrMu.Unlock() if mgr == nil { var err error - mgr, err = NewACMEManager() + mgr, err = internal.NewACMEManager() if err != nil { return nil, err } @@ -31,21 +32,22 @@ func getACMEManager() (*ACMEManager, error) { return mgr, nil } -func initServer() { - err := LoadDomainConfigs() +func Init() { + err := internal.LoadDomainConfigs() if err != nil { log.Fatalf("Error loading domain configs: %v", err) } - serverTick() + Tick() } -func serverTick() { +func Tick() { tickMu.Lock() defer tickMu.Unlock() fmt.Println("Tick!") - mgr, err := getACMEManager() + var err error + mgr, err = getACMEManager() if err != nil { fmt.Printf("Error getting acme manager: %v\n", err) return @@ -53,7 +55,7 @@ func serverTick() { now := time.Now().UTC() - localDomainConfigs := domainStore.Snapshot() + localDomainConfigs := internal.DomainStore().Snapshot() for domainStr, domainConfig := range localDomainConfigs { if !domainConfig.GetBool("Domain.enabled") { @@ -75,62 +77,62 @@ func serverTick() { } domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix()) - err = WriteDomainConfig(domainConfig) + err = internal.WriteDomainConfig(domainConfig) if err != nil { fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) continue } - err = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt.crpt"), nil) + err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".crt.crpt"), nil) if err != nil { fmt.Printf("Error encrypting domain cert for domain %s: %v\n", domainStr, err) continue } - err = EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key.crpt"), nil) + err = internal.EncryptFileXChaCha(domainConfig.GetString("Certificates.crypto_key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.CertsRoot, domainStr, domainStr+".key.crpt"), nil) if err != nil { fmt.Printf("Error encrypting domain key for domain %s: %v\n", domainStr, err) continue } - giteaClient := createGiteaClient() + giteaClient := internal.CreateGiteaClient() if giteaClient == nil { fmt.Printf("Error creating gitea client for domain %s: %v\n", domainStr, err) continue } - gitWorkspace := &GitWorkspace{ + gitWorkspace := &internal.GitWorkspace{ Storage: memory.NewStorage(), FS: memfs.New(), } var repoUrl string if !domainConfig.GetBool("Internal.repo_exists") { - repoUrl = createGiteaRepo(domainStr, giteaClient) + repoUrl = internal.CreateGiteaRepo(domainStr, giteaClient) if repoUrl == "" { fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr) continue } domainConfig.Set("Internal.repo_exists", true) - err = WriteDomainConfig(domainConfig) + err = internal.WriteDomainConfig(domainConfig) if err != nil { fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) continue } - err = initRepo(repoUrl, gitWorkspace) + err = internal.InitRepo(repoUrl, gitWorkspace) if err != nil { fmt.Printf("Error initializing repo for domain %s: %v\n", domainStr, err) continue } } else { - repoUrl = config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" - err = cloneRepo(repoUrl, gitWorkspace) + repoUrl = internal.Config().GetString("Git.server") + "/" + internal.Config().GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" + err = internal.CloneRepo(repoUrl, gitWorkspace, internal.Server) if err != nil { fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err) continue } } - err = addAndPushCerts(domainStr, gitWorkspace) + err = internal.AddAndPushCerts(domainStr, gitWorkspace) if err != nil { fmt.Printf("Error pushing certificates for domain %s: %v\n", domainStr, err) continue @@ -138,15 +140,15 @@ func serverTick() { fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr) } } - err = SaveDomainConfigs() + err = internal.SaveDomainConfigs() if err != nil { fmt.Printf("Error saving domain configs: %v\n", err) } } -func reloadServer() { +func Reload() { fmt.Println("Reloading configs...") - err := LoadDomainConfigs() + err := internal.LoadDomainConfigs() if err != nil { fmt.Printf("Error loading domain configs: %v\n", err) @@ -160,6 +162,6 @@ func reloadServer() { fmt.Println("Successfully reloaded configs") } -func stopServer() { +func Stop() { fmt.Println("Shutting down server") }