diff --git a/.gitignore b/.gitignore index cb8c2a8..b9c42e5 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,4 @@ $RECYCLE.BIN/ config.ini certman +certman-*-amd64 \ No newline at end of file diff --git a/Makefile b/Makefile index eb0b3fd..38a05a3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,13 @@ +VERSION := 1.0.0 + +GO := go + +BUILD_FLAGS := -buildmode=pie -trimpath +LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now" + build: - @go build -o ./certman . + $(GO) build $(BUILD_FLAGS) -ldflags='$(LDFLAGS)' -o ./certman . + @cp ./certman ./certman-$(VERSION)-amd64 stage: build @sudo cp ./certman /srv/vm-passthru/certman diff --git a/acme_manager.go b/acme_manager.go index 7f025aa..349bc12 100644 --- a/acme_manager.go +++ b/acme_manager.go @@ -96,18 +96,9 @@ type StoredCertMeta struct { // - Cloudflare DNS-01 only func NewACMEManager() (*ACMEManager, error) { // Pull effective (main-only) certificate settings. - email, err := config.GetAsStringErr("Certificates.email") - if err != nil { - return nil, err - } - dataRoot, err := config.GetAsStringErr("Certificates.data_root") - if err != nil { - return nil, err - } - caDirURL, err := config.GetAsStringErr("Certificates.ca_dir_url") - if err != nil { - return nil, err - } + email := config.GetString("Certificates.email") + dataRoot := config.GetString("Certificates.data_root") + caDirURL := config.GetString("Certificates.ca_dir_url") // Build manager paths mgr := &ACMEManager{ @@ -246,51 +237,31 @@ func (m *ACMEManager) GetCertPaths(domainKey string) (certPEM, keyPEM string) { // --------------------------------------------- func buildDomainRuntimeConfig(domainKey string) (*DomainRuntimeConfig, error) { - domainCfg, exists := getDomainConfig(domainKey) + domainCfg, exists := domainStore.Get(domainKey) if !exists { return nil, fmt.Errorf("domain config not found for %q", domainKey) } - domainName, err := domainCfg.GetAsStringErr("Domain.domain_name") - if err != nil { - return nil, err - } + domainName := domainCfg.GetString("Domain.domain_name") - email, err := config.GetAsStringErr("Certificates.email") - if err != nil { - return nil, err - } + email := config.GetString("Certificates.email") // domain override data_root can be blank -> main fallback - dataRoot, err := getEffectiveString(domainCfg, "Certificates.data_root") + dataRoot, err := EffectiveString(domainCfg, "Certificates.data_root") if err != nil { return nil, err } - caDirURL, err := config.GetAsStringErr("Certificates.ca_dir_url") - if err != nil { - return nil, err - } + caDirURL := config.GetString("Certificates.ca_dir_url") - expiry, err := domainCfg.GetAsIntErr("Certificates.expiry") - if err != nil { - return nil, err - } + expiry := domainCfg.GetInt("Certificates.expiry") - renewPeriod, err := domainCfg.GetAsIntErr("Certificates.renew_period") - if err != nil { - return nil, err - } + renewPeriod := domainCfg.GetInt("Certificates.renew_period") - requestMethod := "" - if k, err := domainCfg.GetKey("Certificates.request_method"); err == nil && k != nil { - requestMethod = strings.TrimSpace(k.String()) - } + requestMethod := domainCfg.GetString("Certificates.request_method") - subdomains := []string{} - if k, err := domainCfg.GetKey("Certificates.subdomains"); err == nil && k != nil { - subdomains = parseCSVLines(k.String()) - } + subdomains := domainCfg.GetString("Certificates.subdomains") + subdomainArray := parseCSVLines(subdomains) return &DomainRuntimeConfig{ DomainName: domainName, @@ -300,7 +271,7 @@ func buildDomainRuntimeConfig(domainKey string) (*DomainRuntimeConfig, error) { ExpiryDays: expiry, RenewPeriod: renewPeriod, RequestMethod: requestMethod, - Subdomains: subdomains, + Subdomains: subdomainArray, }, nil } @@ -377,14 +348,8 @@ func buildDomainList(baseDomain string, subs []string) []string { func setCloudflareEnvFromMainConfig() (restore func(), err error) { // Your current defaults show legacy email + API key fields. // Prefer API tokens in the future if you add them. Cloudflare provider supports both styles. :contentReference[oaicite:3]{index=3} - cfEmail, err := config.GetAsStringErr("Cloudflare.cf_email") - if err != nil { - return nil, err - } - cfAPIKey, err := config.GetAsStringErr("Cloudflare.cf_api_key") - if err != nil { - return nil, err - } + cfEmail := config.GetString("Cloudflare.cf_email") + cfAPIKey := config.GetString("Cloudflare.cf_api_key") // Save prior env values so we can restore them after provider creation. prevEmail, hadEmail := os.LookupEnv("CLOUDFLARE_EMAIL") diff --git a/client.go b/client.go index 1bae808..9124ab6 100644 --- a/client.go +++ b/client.go @@ -7,45 +7,41 @@ import ( "path/filepath" "strings" - "git.nevets.tech/Steven/ezconf" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5/storage/memory" ) func initClient() { - err := loadDomainConfigs() + err := LoadDomainConfigs() if err != nil { log.Fatalf("Error loading domain configs: %v", err) } + + clientTick() } func clientTick() { fmt.Println("Tick!") // Get local copy of domain configs - mu.RLock() - localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs)) - for k, v := range domainConfigs { - localDomainConfigs[k] = v - } - mu.RUnlock() + localDomainConfigs := domainStore.Snapshot() // Loop over all domain configs (domains) for domainStr, domainConfig := range localDomainConfigs { // Skip non-enabled domains - if !domainConfig.GetAsBoolean("Domain.enabled") { + if !domainConfig.GetBool("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.GetAsBoolean("Internal.repo_exists") + repoExists := domainConfig.GetBool("Internal.repo_exists") if repoExists { localHash, err := getLocalCommitHash(domainStr) if err != nil { fmt.Printf("No local commit hash found for domain %s\n", domainStr) } - gitSource, err := strToGitSource(config.GetAsString("Git.host")) + gitSource, err := strToGitSource(config.GetString("Git.host")) if err != nil { fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err) continue @@ -68,7 +64,7 @@ func clientTick() { } // Ex: https://git.example.com/Org/Repo-suffix.git // Clones repo and stores in gitWorkspace, skip if clone fails (doesn't exist?) - repoUrl := config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domainStr + domainConfig.GetAsString("Repo.repo_suffix") + ".git" + repoUrl := config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" err := cloneRepo(repoUrl, gitWorkspace) if err != nil { fmt.Printf("Error cloning domain repo %s: %v\n", domainStr, err) @@ -108,7 +104,7 @@ func clientTick() { continue } - err = DecryptFileFromBytes(domainConfig.GetAsString("Certificates.crypto_key"), fileBytes, filepath.Join(certsDir, filename), nil) + 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, domainStr, err) continue @@ -133,7 +129,7 @@ func clientTick() { func reloadClient() { fmt.Println("Reloading configs...") - err := loadDomainConfigs() + err := LoadDomainConfigs() if err != nil { fmt.Printf("Error loading domain configs: %v\n", err) return diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..18e0f54 --- /dev/null +++ b/commands.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" +) + +func devCmd(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 versionCmd(cmd *cobra.Command, args []string) { + fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build) +} + +func newKeyCmd(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) + fmt.Println("Successfully created domain entry for " + domain + "\nUpdate config file as needed in /etc/certman/domains/" + domainDir + ".conf") + return nil +} + +func install(isThin bool, mode string) error { + if !isThin { + makeDirs() + } + //config, err = ezconf.NewConfiguration(configFile, strings.ReplaceAll(defaultConfig, "{mode}", mode)) + createNewConfig(mode) + return nil +} + +func runDaemonCmd() 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) + + 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() + } + } + }() + } 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 stopDaemonCmd() 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 reloadDaemonCmd() 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 statusDaemonCmd() error { + fmt.Println("Not implemented :/") + return nil +} diff --git a/config.go b/config.go index 923af76..b771596 100644 --- a/config.go +++ b/config.go @@ -9,173 +9,258 @@ import ( "strings" "sync" - "git.nevets.tech/Steven/ezconf" - "gopkg.in/ini.v1" + "github.com/spf13/viper" ) -var domainConfigs map[string]*ezconf.Configuration -var mu sync.RWMutex - var ( - BlankConfigEntry = errors.New("blank config entry") - ConfigNotFound = errors.New("config file not found") + ErrBlankConfigEntry = errors.New("blank config entry") + ErrConfigNotFound = errors.New("config file not found") ) -func makeDirs() { - err := os.MkdirAll("/etc/certman", 0644) - if err != nil { - if !os.IsExist(err) { - fmt.Println("Unable to create config directory") - os.Exit(1) - } - } +type DomainConfigStore struct { + mu sync.RWMutex + configs map[string]*viper.Viper +} - err = os.Mkdir("/etc/certman/domains", 0644) - if err != nil { - if !os.IsExist(err) { - fmt.Println("Unable to create config directory") - os.Exit(1) - } - } - - err = os.Mkdir("/var/local/certman", 0640) - if err != nil { - if !os.IsExist(err) { - fmt.Printf("Unable to create certman directory: %v\n", err) - os.Exit(1) - } +func NewDomainConfigStore() *DomainConfigStore { + return &DomainConfigStore{ + configs: make(map[string]*viper.Viper), } } -func createNewDomainConfig(domain string) { - key, err := GenerateKey() - if err != nil { - log.Fatalf("Unable to generate key: %v\n", err) - } - data := []byte(strings.ReplaceAll(strings.ReplaceAll(defaultDomainConfig, "{domain}", domain), "{key}", key)) - createFile("/etc/certman/domains/"+domain+".conf", 0640, data) +func (s *DomainConfigStore) Get(domain string) (*viper.Viper, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + v, ok := s.configs[domain] + return v, ok } -func createNewDomainCertsDir(domain string, dir string) { - var err error - if dir == "/opt/certs/example.com" { - err = os.MkdirAll("/var/local/certman/certificates/"+domain, 0640) - } else { - if strings.HasSuffix(dir, "/") { - err = os.MkdirAll(dir+domain, 0640) - } else { - err = os.Mkdir(dir+"/"+domain, 0640) - } - } - if err != nil { - if os.IsExist(err) { - fmt.Println("Directory already exists...") - } else { - log.Fatalf("Error creating certificate directory for %s: %v\n", domain, err) - } - } +func (s *DomainConfigStore) Set(domain string, v *viper.Viper) { + s.mu.Lock() + defer s.mu.Unlock() + s.configs[domain] = v } -func loadDomainConfigs() error { - tempDomainConfigs := make(map[string]*ezconf.Configuration) - entries, err := os.ReadDir("/etc/certman/domains/") - if err != nil { - return err +// Swap atomically replaces the entire config map (used during reload). +func (s *DomainConfigStore) Swap(newConfigs map[string]*viper.Viper) { + s.mu.Lock() + defer s.mu.Unlock() + s.configs = newConfigs +} + +// Snapshot returns a shallow copy safe to iterate without holding the lock. +func (s *DomainConfigStore) Snapshot() map[string]*viper.Viper { + s.mu.RLock() + defer s.mu.RUnlock() + snap := make(map[string]*viper.Viper, len(s.configs)) + for k, v := range s.configs { + snap[k] = v } + return snap +} + +// --------------------------------------------------------------------------- +// Global state +// --------------------------------------------------------------------------- + +var ( + config *viper.Viper + domainStore = NewDomainConfigStore() +) + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +// LoadConfig reads the main certman.conf into config. +func LoadConfig(path string) error { + config = viper.New() + config.SetConfigFile(path) + config.SetConfigType("ini") + return config.ReadInConfig() +} + +// LoadDomainConfigs reads every .conf file in the domains directory. +func LoadDomainConfigs() error { + dir := "/etc/certman/domains/" + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("reading domain config dir: %w", err) + } + + temp := make(map[string]*viper.Viper) for _, entry := range entries { if entry.IsDir() || filepath.Ext(entry.Name()) != ".conf" { continue } - domainConf, err := ezconf.LoadConfiguration("/etc/certman/domains/" + entry.Name()) - if err != nil { - return err - } - domain, err := domainConf.GetAsStringErr("Domain.domain_name") - if err != nil { - return err + path := filepath.Join(dir, entry.Name()) + v := viper.New() + v.SetConfigFile(path) + v.SetConfigType("ini") + + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("loading %s: %w", path, err) } - if _, exists := tempDomainConfigs[domain]; exists { - fmt.Printf("Duplicate domain found in %s, skipping...\n", "/etc/certman/domains/"+entry.Name()) + domain := v.GetString("domain.domain_name") + if domain == "" { + return fmt.Errorf("%s: missing domain.domain_name", path) + } + + if _, exists := temp[domain]; exists { + fmt.Printf("Duplicate domain in %s, skipping...\n", path) continue } - tempDomainConfigs[domain] = domainConf + temp[domain] = v } - mu.Lock() - domainConfigs = tempDomainConfigs - mu.Unlock() + + domainStore.Swap(temp) return nil } -func saveDomainConfigs() { - mu.RLock() - localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs)) - for k, v := range domainConfigs { - localDomainConfigs[k] = v - } - mu.RUnlock() +// --------------------------------------------------------------------------- +// Saving +// --------------------------------------------------------------------------- - for domainStr, domainConfig := range localDomainConfigs { - err := domainConfig.Save() - if err != nil { - fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) - continue +// SaveDomainConfigs writes every loaded domain config back to disk. +func SaveDomainConfigs() { + for domain, v := range domainStore.Snapshot() { + if err := v.WriteConfig(); err != nil { + fmt.Printf("Error saving domain config %s: %v\n", domain, err) } } } -func getDomainConfig(domain string) (*ezconf.Configuration, bool) { - mu.RLock() - defer mu.RUnlock() - funcDomainConfig, exists := domainConfigs[domain] - return funcDomainConfig, exists -} +// --------------------------------------------------------------------------- +// Effective lookups (domain → global fallback) +// --------------------------------------------------------------------------- -func getEffectiveKey(domainConfig *ezconf.Configuration, path string) *ini.Key { - key, err := getEffectiveKeyErr(domainConfig, path) - if err != nil { - if errors.Is(err, BlankConfigEntry) { - return &ini.Key{} - } - fmt.Printf("Error getting value for %s: %v\n", path, err) - return nil - } - return key -} - -func getEffectiveKeyErr(domainConfig *ezconf.Configuration, path string) (*ini.Key, error) { - if domainConfig != nil { - if key, err := domainConfig.GetKey(path); err == nil && key != nil { - if strings.TrimSpace(key.String()) != "" { - return key, nil - } +// EffectiveString looks up a key in the domain config first, falling back to +// the global config. Keys use dot notation matching INI sections, e.g. +// "certificates.data_root". +func EffectiveString(domainCfg *viper.Viper, key string) (string, error) { + if domainCfg != nil { + val := strings.TrimSpace(domainCfg.GetString(key)) + if val != "" { + return val, nil } } - key, err := config.GetKey(path) - if err != nil { - fmt.Printf("Error getting key for %s: %v\n", path, err) - return nil, err - } - if strings.TrimSpace(key.String()) == "" { - return nil, BlankConfigEntry + if config == nil { + return "", ErrConfigNotFound } - return key, nil + val := strings.TrimSpace(config.GetString(key)) + if val == "" { + return "", ErrBlankConfigEntry + } + return val, nil } -func getEffectiveString(domainCfg *ezconf.Configuration, path string) (string, error) { - k, err := getEffectiveKeyErr(domainCfg, path) +// MustEffectiveString is like EffectiveString but logs a fatal error on failure. +func MustEffectiveString(domainCfg *viper.Viper, key string) string { + val, err := EffectiveString(domainCfg, key) if err != nil { - return "", err + log.Fatalf("Config key %q: %v", key, err) } - return strings.TrimSpace(k.String()), nil + return val } +// EffectiveInt returns an int with domain → global fallback. Returns the +// fallback value if the key is missing or zero in both configs. +func EffectiveInt(domainCfg *viper.Viper, key string, fallback int) int { + if domainCfg != nil { + if val := domainCfg.GetInt(key); val != 0 { + return val + } + } + if config != nil { + if val := config.GetInt(key); val != 0 { + return val + } + } + return fallback +} + +// EffectiveBool returns a bool with domain → global fallback. +func EffectiveBool(domainCfg *viper.Viper, key string) bool { + if domainCfg != nil && domainCfg.IsSet(key) { + return domainCfg.GetBool(key) + } + if config != nil { + return config.GetBool(key) + } + return false +} + +// --------------------------------------------------------------------------- +// Directory bootstrapping +// --------------------------------------------------------------------------- + +func makeDirs() { + dirs := []struct { + path string + perm os.FileMode + }{ + {"/etc/certman", 0755}, + {"/etc/certman/domains", 0755}, + {"/var/local/certman", 0750}, + } + + for _, d := range dirs { + if err := os.MkdirAll(d.path, d.perm); err != nil { + log.Fatalf("Unable to create directory %s: %v", d.path, err) + } + } +} + +func createNewConfig(mode string) { + content := strings.NewReplacer("{mode}", mode).Replace(defaultConfig) + createFile("/etc/certman/certman.conf", 640, []byte(content)) +} + +func createNewDomainConfig(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) + + path := filepath.Join("/etc/certman/domains", domain+".conf") + createFile(path, 0640, []byte(content)) + return nil +} + +func createNewDomainCertsDir(domain string, dir string, dirOverride bool) { + var target string + if dirOverride { + target = filepath.Join(dir, domain) + } else { + target = filepath.Join("/var/local/certman/certificates", domain) + } + + if err := os.MkdirAll(target, 0750); err != nil { + if os.IsExist(err) { + fmt.Println("Directory already exists...") + return + } + log.Fatalf("Error creating certificate directory for %s: %v", domain, err) + } +} + +// --------------------------------------------------------------------------- +// Default config templates +// --------------------------------------------------------------------------- + const defaultConfig = `[App] mode = {mode} +tick_rate = 2 [Git] host = gitea @@ -197,7 +282,6 @@ cf_api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxx const defaultDomainConfig = `[Domain] domain_name = {domain} enabled = true -; default (use system dns) or IPv4 Address (1.1.1.1) dns_server = default @@ -217,9 +301,10 @@ crypto_key = {key} repo_suffix = -certificates -; Don't change setting below here unless you know what you're doing! [Internal] last_issued = 0 repo_exists = false status = clean ` + +const readme = `` diff --git a/crypto.go b/crypto.go index 120c302..3bfd532 100644 --- a/crypto.go +++ b/crypto.go @@ -8,7 +8,6 @@ import ( "io" "os" - _ "filippo.io/age" "golang.org/x/crypto/chacha20poly1305" ) diff --git a/git.go b/git.go index 6d877b6..f16a2c5 100644 --- a/git.go +++ b/git.go @@ -10,7 +10,6 @@ import ( "time" "code.gitea.io/sdk/gitea" - "git.nevets.tech/Steven/ezconf" "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" gitconf "github.com/go-git/go-git/v5/config" @@ -18,6 +17,7 @@ import ( "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 GitWorkspace struct { @@ -57,11 +57,11 @@ func strToGitSource(s string) (GitSource, error) { } func createGithubClient() *github.Client { - return github.NewClient(nil).WithAuthToken(config.GetAsString("Git.api_token")) + return github.NewClient(nil).WithAuthToken(config.GetString("Git.api_token")) } func createGiteaClient() *gitea.Client { - client, err := gitea.NewClient(config.GetAsString("Git.server"), gitea.SetToken(config.GetAsString("Git.api_token"))) + 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) return nil @@ -71,7 +71,7 @@ func createGiteaClient() *gitea.Client { func createGithubRepo(domain *Domain, client *github.Client) string { name := domain.name - owner := domain.config.GetAsString("Repo.owner") + owner := domain.config.GetString("Repo.owner") description := domain.description private := true includeAllBranches := false @@ -84,7 +84,7 @@ func createGithubRepo(domain *Domain, client *github.Client) string { Private: &private, IncludeAllBranches: &includeAllBranches, } - repo, _, err := client.Repositories.CreateFromTemplate(ctx, config.GetAsString("Git.org_name"), config.GetAsString("Git.template_name"), template) + 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 "" @@ -93,7 +93,7 @@ func createGithubRepo(domain *Domain, client *github.Client) string { } func createGiteaRepo(domain string, giteaClient *gitea.Client) string { - domainConfig, exists := getDomainConfig(domain) + domainConfig, exists := domainStore.Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) return "" @@ -111,7 +111,7 @@ func createGiteaRepo(domain string, giteaClient *gitea.Client) string { // Webhooks: true, //} options := gitea.CreateRepoOption{ - Name: domain + domainConfig.GetAsString("Repo.repo_suffix"), + Name: domain + domainConfig.GetString("Repo.repo_suffix"), Description: "Certificate storage for " + domain, Private: true, IssueLabels: "", @@ -124,8 +124,7 @@ func createGiteaRepo(domain string, giteaClient *gitea.Client) string { TrustModel: gitea.TrustModelDefault, } - giteaRepo, _, err := giteaClient.CreateOrgRepo(config.GetAsString("Git.org_name"), options) - //giteaRepo, _, err := giteaClient.CreateRepoFromTemplate(config.GetAsString("Git.org_name"), config.GetAsString("Git.template_name"), options) + giteaRepo, _, err := giteaClient.CreateOrgRepo(config.GetString("Git.org_name"), options) if err != nil { fmt.Printf("Error creating repo: %v\n", err) return "" @@ -161,8 +160,8 @@ func initRepo(url string, ws *GitWorkspace) error { func cloneRepo(url string, ws *GitWorkspace) error { creds := &http.BasicAuth{ - Username: config.GetAsString("Git.username"), - Password: config.GetAsString("Git.api_token"), + Username: config.GetString("Git.username"), + Password: config.GetString("Git.api_token"), } var err error ws.Repo, err = git.Clone(ws.Storage, ws.FS, &git.CloneOptions{URL: url, Auth: creds}) @@ -179,15 +178,15 @@ func cloneRepo(url string, ws *GitWorkspace) error { } func addAndPushCerts(domain string, ws *GitWorkspace) error { - domainConfig, exists := getDomainConfig(domain) + domainConfig, exists := domainStore.Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) - return ConfigNotFound + return ErrConfigNotFound } certsDir, err := getDomainCertsDirWConf(domain, domainConfig) if err != nil { - if errors.Is(err, ConfigNotFound) { + if errors.Is(err, ErrConfigNotFound) { fmt.Printf("Domain %s config not found: %v\n", domain, err) return err } @@ -244,7 +243,7 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { fmt.Println("Work Tree Status:\n" + status.String()) signature := &object.Signature{ Name: "Cert Manager", - Email: config.GetAsString("Certificates.email"), + Email: config.GetString("Certificates.email"), When: time.Now(), } commitHash, err := ws.WorkTree.Commit("Update "+domain+" @ "+time.Now().Format("Mon Jan _2 2006 15:04:05 MST"), &git.CommitOptions{Author: signature, Committer: signature}) @@ -253,8 +252,8 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { return err } creds := &http.BasicAuth{ - Username: config.GetAsString("Git.username"), - Password: config.GetAsString("Git.api_token"), + Username: config.GetString("Git.username"), + Password: config.GetString("Git.api_token"), } err = ws.Repo.Push(&git.PushOptions{ Auth: creds, @@ -269,7 +268,7 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { return err } - fmt.Println("Successfully uploaded to " + config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domain + domainConfig.GetAsString("Repo.repo_suffix") + ".git") + fmt.Println("Successfully uploaded to " + config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domain + domainConfig.GetString("Repo.repo_suffix") + ".git") err = writeCommitHash(commitHash.String(), domainConfig) if err != nil { @@ -280,10 +279,10 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { return nil } -func writeCommitHash(hash string, domainConfig *ezconf.Configuration) error { +func writeCommitHash(hash string, domainConfig *viper.Viper) error { certsDir, err := getDomainCertsDirWOnlyConf(domainConfig) if err != nil { - if errors.Is(err, ConfigNotFound) { + if errors.Is(err, ErrConfigNotFound) { return err } return err @@ -300,7 +299,7 @@ func writeCommitHash(hash string, domainConfig *ezconf.Configuration) error { func getLocalCommitHash(domain string) (string, error) { certsDir, err := getDomainCertsDir(domain) if err != nil { - if errors.Is(err, ConfigNotFound) { + if errors.Is(err, ErrConfigNotFound) { fmt.Printf("Domain %s config not found: %v\n", domain, err) return "", err } @@ -317,15 +316,15 @@ func getLocalCommitHash(domain string) (string, error) { } func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) { - domainConfig, exists := getDomainConfig(domain) + domainConfig, exists := domainStore.Get(domain) if !exists { fmt.Printf("Domain %s config does not exist\n", domain) - return "", ConfigNotFound + return "", ErrConfigNotFound } switch gitSource { case Gitea: - return getRemoteCommitHashGitea(config.GetAsString("Git.org_name"), domain+domainConfig.GetAsString("Repo.repo_suffix"), "master") + return getRemoteCommitHashGitea(config.GetString("Git.org_name"), domain+domainConfig.GetString("Repo.repo_suffix"), "master") default: fmt.Printf("Unimplemented git source %v\n", gitSource) return "", errors.New("unimplemented git source") diff --git a/go.mod b/go.mod index a01c641..d4de304 100644 --- a/go.mod +++ b/go.mod @@ -6,13 +6,12 @@ toolchain go1.24.7 require ( code.gitea.io/sdk/gitea v0.15.1 - filippo.io/age v1.2.1 - git.nevets.tech/Steven/ezconf v0.1.4 github.com/go-acme/lego/v4 v4.26.0 github.com/go-git/go-billy/v5 v5.4.1 github.com/go-git/go-git/v5 v5.7.0 github.com/google/go-github/v55 v55.0.0 - github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.18.2 golang.org/x/crypto v0.42.0 ) @@ -23,19 +22,35 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.15 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/miekg/dns v1.1.68 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.1.1 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.44.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -44,4 +59,5 @@ require ( golang.org/x/tools v0.36.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5b9f5d0..2fcbd67 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,6 @@ -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= -c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= -filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= -filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= -git.nevets.tech/Steven/ezconf v0.1.1 h1:dEqV9Q0zVKX9UkPg5UTchGLd0J0WhiuV4dVg0o3blnY= -git.nevets.tech/Steven/ezconf v0.1.1/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI= -git.nevets.tech/Steven/ezconf v0.1.2 h1:KD47Av0swRPHKLxmDtJwahZd+x0k902CgNqBVQcxf2w= -git.nevets.tech/Steven/ezconf v0.1.2/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI= -git.nevets.tech/Steven/ezconf v0.1.3 h1:l9yG5SwYx/Jg4HzkikOsJ5FTPS9BTLGDBxTPgVOovLI= -git.nevets.tech/Steven/ezconf v0.1.3/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI= -git.nevets.tech/Steven/ezconf v0.1.4 h1:W9AHcnWQfmkc1PAlrRj54u3zPq1BXeX3u37X/+Y746g= -git.nevets.tech/Steven/ezconf v0.1.4/go.mod h1:O8svyJLWVPYdxPeZeiTkfmwz77BM0Wq2ZhDrHtdRhvI= github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= @@ -29,6 +17,7 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -38,6 +27,10 @@ github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3O github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-acme/lego/v4 v4.26.0 h1:521aEQxNstXvPQcFDDPrJiFfixcCQuvAvm35R4GbyYA= @@ -64,26 +57,35 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313 h1:5/CjuZQWnRALu4hkEDRg4fA5lWDSfjlKg+koRDRuotQ= -github.com/makifdb/pidfile v0.0.0-20231129022650-50ec86392313/go.mod h1:nm72+BE0Z1PcotR9soX+NnGyEs1iQ0b1Ot0IhL2Nwwk= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -91,20 +93,49 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -112,6 +143,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= +golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -183,5 +216,6 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 6186d4b..9843be7 100644 --- a/main.go +++ b/main.go @@ -2,303 +2,136 @@ package main import ( "context" - "errors" - "flag" "fmt" - "log" "os" - "os/signal" - "strings" "sync" - "syscall" - "time" - "git.nevets.tech/Steven/ezconf" + "github.com/spf13/cobra" ) var version = "1.0.0" -var build = "1" +var build = "2" -var config *ezconf.Configuration - -var ctx context.Context -var cancel context.CancelFunc -var wg sync.WaitGroup +var ( + configFile string + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +) //TODO create logic for gh vs gt repos func main() { - - devFlag := flag.Bool("dev", false, "Developer Mode") - - versionFlag := flag.Bool("version", false, "Show version") - helpFlag := flag.Bool("help", false, "Show help") - - configFile := flag.String("config", "/etc/certman/certman.conf", "Configuration file") - - newDomainFlag := flag.String("new-domain", "example.com", "Domain to create new configs and directories for") - newDomainDirFlag := flag.String("new-domain-dir", "/opt/certs/example.com", "Directory that certs will be stored in") - - installFlag := flag.Bool("install", false, "Install Certman") - modeFlag := flag.String("mode", "client", "CertManager Mode [server, client]") - thinInstallFlag := flag.Bool("t", false, "Thin Install (skip creating dirs)") - - newKeyFlag := flag.Bool("newkey", false, "Generate new encryption key") - - reloadFlag := flag.Bool("reload", false, "Reload configs") - stopFlag := flag.Bool("stop", false, "Stop certman") - - daemonFlag := flag.Bool("d", false, "Daemon Mode") - - flag.Parse() - - if *devFlag { - testDomain := "lunamc.org" - var err error - config, err = ezconf.LoadConfiguration("/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) - - os.Exit(0) + rootCmd := &cobra.Command{ + Use: "certman", + Short: "CertMan", + Long: "Certificate Manager", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, } - if *versionFlag { - fmt.Println("CertManager (certman) - Steven Tracey\nVersion: " + version + " build-" + build) - os.Exit(0) + rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "/etc/certman/certman.conf", "Configuration file") + + rootCmd.AddCommand(basicCmd("version", "Show version", versionCmd)) + rootCmd.AddCommand(basicCmd("gen-key", "Generates encryption key", newKeyCmd)) + rootCmd.AddCommand(basicCmd("dev", "Dev Function", devCmd)) + + var domainCertDir string + newDomainCmd := &cobra.Command{ + Use: "new-domain", + Short: "Create config and directories for new domain", + Args: cobra.ExactArgs(1), + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + dirOverridden := cmd.Flags().Changed("dir") + return newDomain(args[0], domainCertDir, dirOverridden) + }, } + newDomainCmd.Flags().StringVar(&domainCertDir, "dir", "/var/local/certman/certificates/", "Alternate directory for certificates") + rootCmd.AddCommand(newDomainCmd) - if *helpFlag { - fmt.Printf(`CertManager (certman) - Steven Tracey -Version: %s build-%s - -Subcommands: certman -subcommand - - version Shows the current version and build - - help Displays this help message - - newkey Creates a new random 256 bit base64 key - -Daemon Controls: certman -command - - d Start in daemon mode - - reload Reload configs - - stop Stop Daemon - -Installation: certman -install -mode (mode) [-t] [-config /path/to/file] - - install - - mode [mode] Uses the specified config file [server, client] - - t Thin install (skip creating directories) - - config /path/to/file Create config file at the specified path - -New Domain Options: certman -new-domain example.com [-new-domain-dir /path/to/certs] - - new-domain Creates a new domain config - - new-domain-dir Specifies directory for new domain certificates to be stored - -`, version, build) - - os.Exit(0) - } - - if *newDomainFlag != "example.com" { - fmt.Printf("Creating new domain %s\n", *newDomainFlag) - createNewDomainConfig(*newDomainFlag) - createNewDomainCertsDir(*newDomainFlag, *newDomainDirFlag) - fmt.Println("Successfully created domain entry for " + *newDomainFlag + "\nUpdate config file as needed in /etc/certman/domains/" + *newDomainFlag + ".conf") - os.Exit(0) - } - - if *installFlag { - if !*thinInstallFlag { - makeDirs() - } - var err error - config, err = ezconf.NewConfiguration(*configFile, strings.ReplaceAll(defaultConfig, "{mode}", *modeFlag)) - if err != nil { - log.Fatalf("Error creating config: %s\n", err) - } - os.Exit(0) - } - - if *newKeyFlag { - key, err := GenerateKey() - if err != nil { - log.Fatalf("%v", err) - } - fmt.Printf(key) - os.Exit(0) - } - - if *reloadFlag { - proc, err := getDaemonProcess() - if err != nil { - log.Fatalf("Error getting daemon process: %v", err) - } - - err = proc.Signal(syscall.SIGHUP) - if err != nil { - log.Fatalf("Error sending SIGHUP to daemon PID: %v\n", err) - } - os.Exit(0) - } - - if *stopFlag { - proc, err := getDaemonProcess() - if err != nil { - log.Fatalf("Error getting daemon process: %v", err) - } - - err = proc.Signal(syscall.SIGTERM) - if err != nil { - log.Fatalf("Error sending SIGTERM to daemon PID: %v\n", err) - } - os.Exit(0) - } - - if *daemonFlag { - err := createOrUpdatePIDFile("/var/run/certman.pid") - if err != nil { - if errors.Is(err, ErrorPIDInUse) { - log.Fatalf("Deemon process is already running\n") + var ( + modeFlag string + thinInstallFlag bool + ) + installCmd := &cobra.Command{ + Use: "install", + Short: "Create certman files and directories", + RunE: func(cmd *cobra.Command, args []string) error { + switch modeFlag { + case "server", "client": + return install(thinInstallFlag, modeFlag) + default: + return fmt.Errorf("invalid --mode %q (must be server or client)", modeFlag) } - log.Fatalf("Error creating pidfile: %v\n", err) - } + }, + } + installCmd.Flags().StringVar(&modeFlag, "mode", "client", "CertManager mode [server, client]") + installCmd.Flags().BoolVarP(&thinInstallFlag, "thin", "t", false, "Thin install (skip creating dirs)") + rootCmd.AddCommand(installCmd) - ctx, cancel = context.WithCancel(context.Background()) + daemonCmd := &cobra.Command{ + Use: "daemon", + Short: "Daemon management", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } - // Check if main config exists - if _, err := os.Stat(*configFile); os.IsNotExist(err) { - log.Fatalf("Main config file not found, please run 'certman --install', then properly configure /etc/certman/certman.conf.") - } else if err != nil { - fmt.Printf("Error opening %s: %v\n", *configFile, err) - } - config, err = ezconf.LoadConfiguration(*configFile) - if err != nil { - log.Fatalf("Error loading configuration: %v\n", err) - } + daemonCmd.AddCommand(&cobra.Command{ + Use: "start", + Short: "Start the daemon", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runDaemonCmd() + }, + }) - // Setup SIGINT and SIGTERM listeners - sigChannel := make(chan os.Signal, 1) - signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM) - defer signal.Stop(sigChannel) + daemonCmd.AddCommand(&cobra.Command{ + Use: "stop", + Short: "Stop the daemon", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return stopDaemonCmd() + }, + }) - reloadSigChan := make(chan os.Signal, 1) - signal.Notify(reloadSigChan, syscall.SIGHUP) - defer signal.Stop(reloadSigChan) + daemonCmd.AddCommand(&cobra.Command{ + Use: "reload", + Short: "Reload daemon configs", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return reloadDaemonCmd() + }, + }) - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() + daemonCmd.AddCommand(&cobra.Command{ + Use: "status", + Short: "Show daemon status", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return statusDaemonCmd() + }, + }) - wg.Add(1) - if config.GetAsString("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() - } - } - }() - } else if config.GetAsString("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 { - fmt.Println("Invalid operating mode \"" + config.GetAsString("App.mode") + "\"") - } + rootCmd.AddCommand(daemonCmd) - // Cleanup on stop - sig := <-sigChannel - fmt.Printf("Program terminated with %v\n", sig) - - stop() - wg.Wait() + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } } -func stop() { - cancel() - clearPIDFile() +func basicCmd(use, short string, commandFunc func(cmd *cobra.Command, args []string)) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Run: commandFunc, + } } -//var legoBaseArgs []string -// -//func maindis() { -// config, err := ezconf.NewConfiguration("/etc/certman/certman.conf", "") -// var domain string -// if err != nil { -// log.Fatalf("Error loading configuration: %v\n", err) -// } -// -// args := os.Args -// -// // -d -// hasDomain, domainIndex := contains(args, "-d") -// if hasDomain { -// domain = args[domainIndex+1] -// } else { -// log.Fatalf("Error, no domain passed. Please add '-d domain.tld' to the command\n") -// } -// -// hasDns, dnsIndex := contains(args, "--dns") -// -// legoBaseArgs = []string{ -// "-a", -// "--dns", -// "cloudflare", -// "--email=" + config.GetAsString("Cloudflare.cf_email"), -// "--domains=" + domain, -// "--domains=*." + domain, -// "--path=" + config.GetAsString("Certificates.certs_path"), -// } -// legoNewSiteArgs := append(legoBaseArgs, "run") -// legoRenewSiteArgs := append(legoBaseArgs, "renew", "--days", "90") -// -// subdomains := config.GetAsStrings("Certificates.subdomains") -// if subdomains != nil { -// for i, subdomain := range subdomains { -// legoBaseArgs = insert(legoBaseArgs, 5+i, "--domains=*."+subdomain+"."+domain) -// } -// } -// -// if hasDns { -// legoBaseArgs = insert(legoBaseArgs, 3, "--dns.resolvers="+args[dnsIndex+1]) -// } -// -// giteaClient = createGiteaClient() -// gitWorkspace := &GitWorkspace{ -// Storage: memory.NewStorage(), -// FS: memfs.New(), -// } -// -// var cmd *exec.Cmd -// switch args[len(args)-1] { // case "gen": // { // url := createGiteaRepo(domain) @@ -343,44 +176,3 @@ func stop() { // } // os.Exit(0) // } -// default: -// { -// fmt.Println("Missing arguments: conclude command with 'gen' or 'renew'") -// os.Exit(1) -// } -// } -// cmd.Env = append(cmd.Environ(), -// "CLOUDFLARE_DNS_API_TOKEN="+config.GetAsString("Cloudflare.cf_api_token"), -// "CLOUDFLARE_ZONE_API_TOKEN"+config.GetAsString("Cloudflare.cf_api_token"), -// "CLOUDFLARE_EMAIL="+config.GetAsString("Cloudflare.cf_email"), -// ) -// stdout, err := cmd.StdoutPipe() -// if err != nil { -// fmt.Printf("Error getting stdout from lego process: %v\n", err) -// os.Exit(1) -// } -// err = cmd.Start() -// if err != nil { -// fmt.Printf("Error creating certs with lego: %v\n", err) -// os.Exit(1) -// } -// scanner := bufio.NewScanner(stdout) -// go func() { -// for scanner.Scan() { -// fmt.Println(scanner.Text()) -// } -// if err := scanner.Err(); err != nil { -// fmt.Fprintln(os.Stderr, "reading standard input:", err) -// } -// }() -// err = cmd.Wait() -// if err != nil { -// fmt.Printf("Error waiting for lego command to finish: %v\n", err) -// os.Exit(1) -// } -// err = addAndPushCerts(domain, gitWorkspace) -// if err != nil { -// fmt.Printf("Error adding and pushing certs: %v\n", err) -// return -// } -//} diff --git a/server.go b/server.go index 5ffd411..7132056 100644 --- a/server.go +++ b/server.go @@ -4,11 +4,9 @@ import ( "fmt" "log" "path/filepath" - "strconv" "sync" "time" - "git.nevets.tech/Steven/ezconf" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5/storage/memory" ) @@ -34,10 +32,12 @@ func getACMEManager() (*ACMEManager, error) { } func initServer() { - err := loadDomainConfigs() + err := LoadDomainConfigs() if err != nil { log.Fatalf("Error loading domain configs: %v", err) } + + serverTick() } func serverTick() { @@ -53,18 +53,14 @@ func serverTick() { now := time.Now().UTC() - mu.RLock() - localDomainConfigs := make(map[string]*ezconf.Configuration, len(domainConfigs)) - for k, v := range domainConfigs { - localDomainConfigs[k] = v - } - mu.RUnlock() + localDomainConfigs := domainStore.Snapshot() + for domainStr, domainConfig := range localDomainConfigs { - if !domainConfig.GetAsBoolean("Domain.enabled") { + if !domainConfig.GetBool("Domain.enabled") { continue } - renewPeriod := domainConfig.GetAsInt("Certificates.renew_period") - lastIssued := time.Unix(domainConfig.GetAsInt64("Internal.last_issued"), 0).UTC() + renewPeriod := domainConfig.GetInt("Certificates.renew_period") + lastIssued := time.Unix(domainConfig.GetInt64("Internal.last_issued"), 0).UTC() renewalDue := lastIssued.AddDate(0, 0, renewPeriod) if now.After(renewalDue) { _, err = mgr.RenewForDomain(domainStr) @@ -77,23 +73,19 @@ func serverTick() { } } - err = domainConfig.SetValueErr("Internal.last_issued", strconv.FormatInt(time.Now().UTC().Unix(), 10)) - if err != nil { - fmt.Printf("Error updating last_issued config for domain %s: %v\n", domainStr, err) - continue - } - err = domainConfig.Save() + domainConfig.Set("Internal.last_issued", time.Now().UTC().Unix()) + err = domainConfig.WriteConfig() if err != nil { fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) continue } - err = EncryptFileXChaCha(domainConfig.GetAsString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".crt.crpt"), nil) + err = 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.GetAsString("Certificates.crypto_key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key"), filepath.Join(mgr.certsRoot, domainStr, domainStr+".key.crpt"), nil) + err = 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 @@ -110,18 +102,14 @@ func serverTick() { } var repoUrl string - if !domainConfig.GetAsBoolean("Internal.repo_exists") { + if !domainConfig.GetBool("Internal.repo_exists") { repoUrl = createGiteaRepo(domainStr, giteaClient) if repoUrl == "" { fmt.Printf("Error creating Gitea repo for domain %s\n", domainStr) continue } - err = domainConfig.SetValueErr("Internal.repo_exists", "true") - if err != nil { - fmt.Printf("Error updating repo_exists config for domain %s: %v\n", domainStr, err) - continue - } - err = domainConfig.Save() + domainConfig.Set("Internal.repo_exists", true) + err = domainConfig.WriteConfig() if err != nil { fmt.Printf("Error saving domain config %s: %v\n", domainStr, err) continue @@ -133,7 +121,7 @@ func serverTick() { continue } } else { - repoUrl = config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domainStr + domainConfig.GetAsString("Repo.repo_suffix") + ".git" + repoUrl = config.GetString("Git.server") + "/" + config.GetString("Git.org_name") + "/" + domainStr + domainConfig.GetString("Repo.repo_suffix") + ".git" err = cloneRepo(repoUrl, gitWorkspace) if err != nil { fmt.Printf("Error cloning repo for domain %s: %v\n", domainStr, err) @@ -149,12 +137,12 @@ func serverTick() { fmt.Printf("Successfully pushed certificates for domain %s\n", domainStr) } } - saveDomainConfigs() + SaveDomainConfigs() } func reloadServer() { fmt.Println("Reloading configs...") - err := loadDomainConfigs() + err := LoadDomainConfigs() if err != nil { fmt.Printf("Error loading domain configs: %v\n", err) diff --git a/util.go b/util.go index 39a6a16..8889959 100644 --- a/util.go +++ b/util.go @@ -10,8 +10,8 @@ import ( "syscall" "code.gitea.io/sdk/gitea" - "git.nevets.tech/Steven/ezconf" "github.com/google/go-github/v55/github" + "github.com/spf13/viper" ) var ( @@ -22,7 +22,7 @@ var ( type Domain struct { name *string - config *ezconf.Configuration + config *viper.Viper description *string ghClient *github.Client gtClient *gitea.Client @@ -245,19 +245,19 @@ func sanitizeDomainKey(s string) string { return r.Replace(s) } -// getDomainCertsDir Can return BlankConfigEntry, ConfigNotFound, or other errors +// getDomainCertsDir Can return ErrBlankConfigEntry, ErrConfigNotFound, or other errors func getDomainCertsDir(domain string) (string, error) { - domainConfig, exists := getDomainConfig(domain) + domainConfig, exists := domainStore.Get(domain) if !exists { - return "", ConfigNotFound + return "", ErrConfigNotFound } return getDomainCertsDirWConf(domain, domainConfig) } -// getDomainCertsDir Can return BlankConfigEntry or other errors -func getDomainCertsDirWConf(domain string, domainConfig *ezconf.Configuration) (string, error) { - effectiveDataRoot, err := getEffectiveString(domainConfig, "Certificates.data_root") +// getDomainCertsDir Can return ErrBlankConfigEntry or other errors +func getDomainCertsDirWConf(domain string, domainConfig *viper.Viper) (string, error) { + effectiveDataRoot, err := EffectiveString(domainConfig, "Certificates.data_root") if err != nil { return "", err } @@ -265,7 +265,7 @@ func getDomainCertsDirWConf(domain string, domainConfig *ezconf.Configuration) ( return filepath.Join(effectiveDataRoot, "certificates", domain), nil } -func getDomainCertsDirWOnlyConf(domainConfig *ezconf.Configuration) (string, error) { - domain := domainConfig.GetAsString("Domain.domain_name") +func getDomainCertsDirWOnlyConf(domainConfig *viper.Viper) (string, error) { + domain := domainConfig.GetString("Domain.domain_name") return getDomainCertsDirWConf(domain, domainConfig) }