diff --git a/client.go b/client.go index 7140270..1bae808 100644 --- a/client.go +++ b/client.go @@ -22,6 +22,7 @@ func initClient() { 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 { @@ -29,15 +30,44 @@ func clientTick() { } mu.RUnlock() + // Loop over all domain configs (domains) for domainStr, domainConfig := range localDomainConfigs { + // Skip non-enabled domains if !domainConfig.GetAsBoolean("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") + 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")) + if err != nil { + fmt.Printf("Error getting git source for domain %s: %v\n", domainStr, err) + continue + } + remoteHash, err := getRemoteCommitHash(domainStr, gitSource) + if err != nil { + fmt.Printf("Error getting remote commit hash for domain %s: %v\n", domainStr, err) + } + // If both hashes are blank (errored), break + // If localHash equals remoteHash (local is up-to-date), skip + if !(localHash == "" && remoteHash == "") && localHash == remoteHash { + fmt.Printf("Domain %s is up to date. Skipping...\n", domainStr) + continue + } + } + gitWorkspace := &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.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domainStr + domainConfig.GetAsString("Repo.repo_suffix") + ".git" err := cloneRepo(repoUrl, gitWorkspace) if err != nil { @@ -45,11 +75,19 @@ func clientTick() { continue } + certsDir, err := getDomainCertsDirWConf(domainStr, domainConfig) + if err != nil { + fmt.Printf("Error getting certificates dir for domain %s: %v\n", domainStr, err) + continue + } + + // Get files in repo fileInfos, err := gitWorkspace.FS.ReadDir("/") if err != nil { fmt.Printf("Error reading directory in memFS on domain %s: %v\n", domainStr, err) continue } + // Iterate over files, filtering by .crpt (encrypted) files in case other files were accidentally added for _, fileInfo := range fileInfos { if strings.HasSuffix(fileInfo.Name(), ".crpt") { filename, _ := strings.CutSuffix(fileInfo.Name(), ".crpt") @@ -58,7 +96,6 @@ func clientTick() { fmt.Printf("Error opening file in memFS on domain %s: %v\n", domainStr, err) continue } - fileBytes, err := io.ReadAll(file) if err != nil { fmt.Printf("Error reading file in memFS on domain %s: %v\n", domainStr, err) @@ -71,18 +108,23 @@ func clientTick() { continue } - dataRoot, err := getEffectiveString(domainConfig, "Certificates.data_root") - if err != nil { - fmt.Printf("Error getting effective data_root for domain %s: %v\n", domainStr, err) - continue - } - err = DecryptFileFromBytes(domainConfig.GetAsString("Certificates.crypto_key"), fileBytes, filepath.Join(dataRoot, "certificates", domainStr, filename), nil) + err = DecryptFileFromBytes(domainConfig.GetAsString("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 } - //TODO write hash locally, compare on tick to determine update + headRef, err := gitWorkspace.Repo.Head() + if err != nil { + fmt.Printf("Error getting head reference for domain %s: %v\n", domainStr, err) + continue + } + + err = writeCommitHash(headRef.Hash().String(), domainConfig) + if err != nil { + fmt.Printf("Error writing commit hash: %v\n", err) + continue + } } } } diff --git a/git.go b/git.go index 7d7b080..6d877b6 100644 --- a/git.go +++ b/git.go @@ -10,6 +10,7 @@ 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" @@ -26,6 +27,35 @@ type GitWorkspace struct { WorkTree *git.Worktree } +type GitSource int + +const ( + Github GitSource = iota + Gitlab + Gitea + Gogs + Bitbucket + CodeCommit +) + +var GitSourceName = map[GitSource]string{ + Github: "github", + Gitlab: "gitlab", + Gitea: "gitea", + Gogs: "gogs", + Bitbucket: "bitbucket", + CodeCommit: "code-commit", +} + +func strToGitSource(s string) (GitSource, error) { + for k, v := range GitSourceName { + if v == s { + return k, nil + } + } + return GitSource(0), errors.New("invalid gitsource name") +} + func createGithubClient() *github.Client { return github.NewClient(nil).WithAuthToken(config.GetAsString("Git.api_token")) } @@ -155,13 +185,16 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { return ConfigNotFound } - effectiveDataRoot, err := getEffectiveString(domainConfig, "Certificates.data_root") + certsDir, err := getDomainCertsDirWConf(domain, domainConfig) if err != nil { - fmt.Printf("Error getting effective data root for domain %s: %v\n", domain, err) - return err + if errors.Is(err, ConfigNotFound) { + fmt.Printf("Domain %s config not found: %v\n", domain, err) + return err + } + fmt.Printf("Error getting domain %s certs dir: %v\n", domain, err) } - certFiles, err := os.ReadDir(filepath.Join(effectiveDataRoot, "certificates", domain)) + certFiles, err := os.ReadDir(certsDir) if err != nil { fmt.Printf("Error reading from directory: %v\n", err) return err @@ -173,7 +206,7 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { fmt.Printf("Error copying file to memfs: %v\n", err) return err } - certFile, err := os.ReadFile(filepath.Join(effectiveDataRoot, "certificates", domain, file.Name())) + certFile, err := os.ReadFile(filepath.Join(certsDir, file.Name())) if err != nil { fmt.Printf("Error reading file to memfs: %v\n", err) file.Close() @@ -214,7 +247,7 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { Email: config.GetAsString("Certificates.email"), When: time.Now(), } - _, err = ws.WorkTree.Commit("Update "+domain+" @ "+time.Now().Format("Mon Jan _2 2006 15:04:05 MST"), &git.CommitOptions{Author: signature, Committer: signature}) + commitHash, err := ws.WorkTree.Commit("Update "+domain+" @ "+time.Now().Format("Mon Jan _2 2006 15:04:05 MST"), &git.CommitOptions{Author: signature, Committer: signature}) if err != nil { fmt.Printf("Error committing certs: %v\n", err) return err @@ -237,25 +270,75 @@ func addAndPushCerts(domain string, ws *GitWorkspace) error { } fmt.Println("Successfully uploaded to " + config.GetAsString("Git.server") + "/" + config.GetAsString("Git.org_name") + "/" + domain + domainConfig.GetAsString("Repo.repo_suffix") + ".git") + + err = writeCommitHash(commitHash.String(), domainConfig) + if err != nil { + fmt.Printf("Error writing commit hash: %v\n", err) + return err + } + + return nil +} + +func writeCommitHash(hash string, domainConfig *ezconf.Configuration) error { + certsDir, err := getDomainCertsDirWOnlyConf(domainConfig) + if err != nil { + if errors.Is(err, ConfigNotFound) { + return err + } + return err + } + + err = os.WriteFile(filepath.Join(certsDir, "hash"), []byte(hash), 0644) + if err != nil { + return err + } + return nil } func getLocalCommitHash(domain string) (string, error) { + certsDir, err := getDomainCertsDir(domain) + if err != nil { + if errors.Is(err, ConfigNotFound) { + fmt.Printf("Domain %s config not found: %v\n", domain, err) + return "", err + } + fmt.Printf("Error getting domain %s certs dir: %v\n", domain, err) + } - return "", nil + 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 + } + + return strings.TrimSpace(string(data)), nil } -func writeCommitHash(domain string, ws *GitWorkspace) error { - //ref, err := ws.Repo.Head() - //if err != nil { - // fmt.Printf("Error getting HEAD: %v\n", err) - // return err - //} - //hash := ref.Hash() - return nil +func getRemoteCommitHash(domain string, gitSource GitSource) (string, error) { + domainConfig, exists := getDomainConfig(domain) + if !exists { + fmt.Printf("Domain %s config does not exist\n", domain) + return "", ConfigNotFound + } + + switch gitSource { + case Gitea: + return getRemoteCommitHashGitea(config.GetAsString("Git.org_name"), domain+domainConfig.GetAsString("Repo.repo_suffix"), "master") + default: + fmt.Printf("Unimplemented git source %v\n", gitSource) + return "", errors.New("unimplemented git source") + } } -func getRemoteCommitHash(domain string) (string, error) { - - return "", nil +func getRemoteCommitHashGitea(org, repo, branchName string) (string, error) { + giteaClient := createGiteaClient() + branch, _, err := giteaClient.GetRepoBranch(org, repo, branchName) + if err != nil { + fmt.Printf("Error getting repo branch: %v\n", err) + return "", err + } + //TODO catch repo not found as ErrRepoNotInit + return branch.Commit.ID, nil } diff --git a/main.go b/main.go index edf28d2..6186d4b 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "git.nevets.tech/Steven/ezconf" ) -var version = "1.1.0-beta" +var version = "1.0.0" var build = "1" var config *ezconf.Configuration @@ -38,7 +38,6 @@ func main() { 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") - localOnlyFlag := flag.Bool("local-only", false, "Local only") installFlag := flag.Bool("install", false, "Install Certman") modeFlag := flag.String("mode", "client", "CertManager Mode [server, client]") @@ -98,7 +97,6 @@ Installation: certman -install -mode (mode) [-t] [-config /path/to/file] 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 - - local-only Don't create git repo `, version, build) @@ -109,9 +107,6 @@ New Domain Options: certman -new-domain example.com [-new-domain-dir /path/to/ce fmt.Printf("Creating new domain %s\n", *newDomainFlag) createNewDomainConfig(*newDomainFlag) createNewDomainCertsDir(*newDomainFlag, *newDomainDirFlag) - if !*localOnlyFlag { - //TODO create git repo - } fmt.Println("Successfully created domain entry for " + *newDomainFlag + "\nUpdate config file as needed in /etc/certman/domains/" + *newDomainFlag + ".conf") os.Exit(0) } diff --git a/util.go b/util.go index 1c18515..39a6a16 100644 --- a/util.go +++ b/util.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strconv" "strings" "syscall" @@ -14,8 +15,9 @@ import ( ) var ( - ErrorPIDInUse = errors.New("daemon is already running") - ErrLockFailed = errors.New("failed to acquire a lock on the PID file") + ErrorPIDInUse = errors.New("daemon is already running") + ErrLockFailed = errors.New("failed to acquire a lock on the PID file") + ErrRepoNotInit = errors.New("repo not initialized") ) type Domain struct { @@ -242,3 +244,28 @@ func sanitizeDomainKey(s string) string { r := strings.NewReplacer("/", "_", "\\", "_", " ", "_", ":", "_") return r.Replace(s) } + +// getDomainCertsDir Can return BlankConfigEntry, ConfigNotFound, or other errors +func getDomainCertsDir(domain string) (string, error) { + domainConfig, exists := getDomainConfig(domain) + if !exists { + return "", ConfigNotFound + } + + 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") + if err != nil { + return "", err + } + + return filepath.Join(effectiveDataRoot, "certificates", domain), nil +} + +func getDomainCertsDirWOnlyConf(domainConfig *ezconf.Configuration) (string, error) { + domain := domainConfig.GetAsString("Domain.domain_name") + return getDomainCertsDirWConf(domain, domainConfig) +}