this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: refactoring

Signed-off-by: A. Ottr <alex@otter.foo>

A. Ottr 32979fe7 fee61b84

+222 -203
+6 -3
.nox.yaml
··· 4 4 recipients: 5 5 - "age1qq5sazxv755u2vs5ulyl486jxhlg7ztrvm27nya47aln668xldkqsm4kn5" 6 6 statePath: ".nox-state.json" 7 - defaultRepo: git@github.com:ShorkBytes/nox-secrets.git 8 - 7 + git: 8 + repo: git@github.com:ShorkBytes/nox-secrets.git 9 + branch: main 9 10 apps: 10 11 debug: 11 - branch: main 12 + git: 13 + repo: git@github.com:ShorkBytes/nox-secrets.git 14 + branch: main 12 15 files: 13 16 - path: debug/debug.age 14 17 output: ./secrets/.env
+8
Makefile
··· 1 + .PHONY: build 2 + 3 + build: 4 + @go build -o bin/nox ./cmd/nox/main.go 5 + 6 + .PHONY: run 7 + run: build 8 + @./bin/nox
+6
cmd/nox/main.go
··· 228 228 return processor.ValidateConfig(rtx.Config) 229 229 }, 230 230 }, 231 + { 232 + Name: "init", 233 + Action: func(ctx context.Context, cmd *cli.Command) error { 234 + return config.InitConfig(configPath) 235 + }, 236 + }, 231 237 }, 232 238 } 233 239
+6 -6
internal/cache/repocache.go
··· 3 3 import ( 4 4 "sync" 5 5 6 - "github.com/aottr/nox/internal/gitrepo" 6 + "github.com/aottr/nox/internal/config" 7 + "github.com/aottr/nox/internal/git" 7 8 "github.com/go-git/go-git/v5/plumbing/object" 8 9 ) 9 10 ··· 36 37 c.repos[key] = tree 37 38 } 38 39 39 - func (c *RepoCache) FetchRepo(key RepoKey, token *string) (*object.Tree, error) { 40 - r, err := gitrepo.CloneRepoInMemory(gitrepo.GitFetchOptions{ 41 - RepoURL: key.Repo, 42 - Branch: key.Branch, 43 - Token: token, 40 + func (c *RepoCache) FetchRepo(key RepoKey) (*object.Tree, error) { 41 + r, err := git.CloneRepo(config.GitConfig{ 42 + Repo: key.Repo, 43 + Branch: key.Branch, 44 44 }) 45 45 if err != nil { 46 46 return nil, err
+54 -9
internal/config/config.go
··· 1 1 package config 2 2 3 3 import ( 4 + "fmt" 4 5 "os" 5 6 6 7 "gopkg.in/yaml.v3" ··· 16 17 Output string `yaml:"output,omitempty"` 17 18 } 18 19 20 + type GitConfig struct { 21 + Repo string `yaml:"repo"` 22 + Branch string `yaml:"branch"` 23 + } 24 + 25 + func (g GitConfig) IsValid() bool { 26 + return g.Repo != "" && g.Branch != "" 27 + } 28 + 19 29 type AppConfig struct { 20 - Repo string `yaml:"repo,omitempty"` 21 - Branch string `yaml:"branch"` 22 - Files []FileConfig `yaml:"files"` 30 + GitConfig GitConfig `yaml:"git,omitempty"` 31 + Files []FileConfig `yaml:"files"` 23 32 } 24 33 25 34 type AgeConfig struct { ··· 29 38 } 30 39 31 40 type Config struct { 32 - Interval string `yaml:"interval"` 33 - Age AgeConfig `yaml:"age"` 34 - StatePath string `yaml:"statePath"` 35 - DefaultRepo string `yaml:"defaultRepo"` 36 - Secrets []SecretMapping `yaml:"secrets"` 37 - Apps map[string]AppConfig `yaml:"apps"` 41 + Interval string `yaml:"interval"` 42 + Age AgeConfig `yaml:"age"` 43 + StatePath string `yaml:"statePath"` 44 + GitConfig GitConfig `yaml:"git"` 45 + Apps map[string]AppConfig `yaml:"apps"` 38 46 } 39 47 40 48 func Load(path string) (*Config, error) { ··· 48 56 return nil, err 49 57 } 50 58 59 + // validate config 60 + // must have at least one git config 61 + hasAnyGit := cfg.GitConfig.IsValid() 62 + if !hasAnyGit { 63 + for _, app := range cfg.Apps { 64 + if app.GitConfig.IsValid() { 65 + hasAnyGit = true 66 + break 67 + } 68 + } 69 + } 70 + if !hasAnyGit { 71 + return nil, fmt.Errorf("no git configuration found: set either top-level git or app-specific git") 72 + } 73 + 51 74 return &cfg, nil 52 75 } 76 + 77 + func InitConfig(path string) error { 78 + 79 + _, err := os.Stat(path) 80 + if err == nil { 81 + return fmt.Errorf("config file already exists") 82 + } 83 + 84 + cfg := Config{ 85 + Interval: "10m", 86 + StatePath: ".nox-state.json", 87 + GitConfig: GitConfig{ 88 + Repo: "", 89 + Branch: "main", 90 + }, 91 + } 92 + cfgYaml, err := yaml.Marshal(cfg) 93 + if err != nil { 94 + return fmt.Errorf("failed to marshal config: %w", err) 95 + } 96 + return os.WriteFile(path, cfgYaml, 0600) 97 + }
+38
internal/git/auth.go
··· 1 + package git 2 + 3 + import ( 4 + "os" 5 + 6 + "github.com/go-git/go-git/v5/plumbing/transport" 7 + "github.com/go-git/go-git/v5/plumbing/transport/http" 8 + "github.com/go-git/go-git/v5/plumbing/transport/ssh" 9 + ) 10 + 11 + func GetAuth() (transport.AuthMethod, error) { 12 + 13 + if sshKey, exists := os.LookupEnv("NOX_GIT_SSH_KEY_FILE"); exists { 14 + pemBytes, err := os.ReadFile(sshKey) 15 + if err != nil { 16 + return nil, err 17 + } 18 + passPhrase := "" 19 + if sshPass, exists := os.LookupEnv("NOX_GIT_SSH_KEY_PASSWORD"); exists { 20 + passPhrase = sshPass 21 + } 22 + 23 + pubKey, err := ssh.NewPublicKeys("git", pemBytes, passPhrase) 24 + if err != nil { 25 + return nil, err 26 + } 27 + 28 + return pubKey, nil 29 + } 30 + 31 + if gitToken, exists := os.LookupEnv("NOX_GIT_TOKEN"); exists { 32 + return &http.BasicAuth{ 33 + Username: "nox", 34 + Password: gitToken, 35 + }, nil 36 + } 37 + return nil, nil 38 + }
+85
internal/git/repo.go
··· 1 + package git 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + 7 + "github.com/aottr/nox/internal/config" 8 + "github.com/go-git/go-git/v5" 9 + "github.com/go-git/go-git/v5/plumbing" 10 + "github.com/go-git/go-git/v5/plumbing/object" 11 + "github.com/go-git/go-git/v5/storage/memory" 12 + ) 13 + 14 + type ClonedRepo struct { 15 + Repo *git.Repository 16 + Tree *object.Tree 17 + Ref *plumbing.Reference 18 + Commit *object.Commit 19 + } 20 + 21 + func CloneRepo(c config.GitConfig) (*ClonedRepo, error) { 22 + 23 + auth, err := GetAuth() 24 + if err != nil { 25 + return nil, err 26 + } 27 + 28 + cloneOpts := &git.CloneOptions{ 29 + URL: c.Repo, 30 + SingleBranch: true, 31 + Depth: 1, 32 + ReferenceName: plumbing.NewBranchReferenceName(c.Branch), 33 + Auth: auth, 34 + } 35 + 36 + repo, err := git.Clone(memory.NewStorage(), nil, cloneOpts) 37 + if err != nil { 38 + return nil, fmt.Errorf("clone failed: %w", err) 39 + } 40 + ref, err := repo.Head() 41 + if err != nil { 42 + return nil, fmt.Errorf("failed to get HEAD: %w", err) 43 + } 44 + 45 + commit, err := repo.CommitObject(ref.Hash()) 46 + if err != nil { 47 + return nil, fmt.Errorf("failed to get commit: %w", err) 48 + } 49 + 50 + tree, err := commit.Tree() 51 + if err != nil { 52 + return nil, fmt.Errorf("failed to get tree: %w", err) 53 + } 54 + return &ClonedRepo{ 55 + Repo: repo, 56 + Ref: ref, 57 + Commit: commit, 58 + Tree: tree, 59 + }, nil 60 + } 61 + 62 + func GetFileContentFromTree(tree *object.Tree, path string) ([]byte, error) { 63 + file, err := tree.File(path) 64 + if err != nil { 65 + return nil, fmt.Errorf("file %q not found: %w", path, err) 66 + } 67 + 68 + reader, err := file.Blob.Reader() 69 + if err != nil { 70 + return nil, fmt.Errorf("failed to open reader for %q: %w", path, err) 71 + } 72 + defer reader.Close() 73 + 74 + content, err := io.ReadAll(reader) 75 + if err != nil { 76 + return nil, fmt.Errorf("failed to read content of %q: %w", path, err) 77 + } 78 + 79 + return content, nil 80 + } 81 + 82 + func FileExistsInTree(tree *object.Tree, path string) bool { 83 + _, err := tree.File(path) 84 + return err == nil 85 + }
-158
internal/gitrepo/memfetch.go
··· 1 - package gitrepo 2 - 3 - import ( 4 - "fmt" 5 - "io" 6 - "os" 7 - 8 - git "github.com/go-git/go-git/v5" 9 - "github.com/go-git/go-git/v5/plumbing" 10 - "github.com/go-git/go-git/v5/plumbing/object" 11 - gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http" 12 - gitMem "github.com/go-git/go-git/v5/storage/memory" 13 - ) 14 - 15 - type GitFetchOptions struct { 16 - RepoURL string 17 - Branch string 18 - File string 19 - Token *string // optional 20 - } 21 - 22 - type ClonedRepo struct { 23 - Repo *git.Repository 24 - Tree *object.Tree 25 - Ref *plumbing.Reference 26 - Commit *object.Commit 27 - } 28 - 29 - func CloneRepoInMemory(opts GitFetchOptions) (*ClonedRepo, error) { 30 - 31 - cloneOpts := &git.CloneOptions{ 32 - URL: opts.RepoURL, 33 - SingleBranch: true, 34 - Depth: 1, 35 - ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), 36 - } 37 - 38 - // check for token authentication 39 - token := opts.Token 40 - if token == nil { 41 - if envToken, exists := os.LookupEnv("GIT_TOKEN"); exists { 42 - token = &envToken 43 - } 44 - } 45 - if token != nil { 46 - cloneOpts.Auth = &gitHttp.BasicAuth{ 47 - Username: "nox", 48 - Password: *token, 49 - } 50 - } 51 - 52 - repo, err := git.Clone(gitMem.NewStorage(), nil, cloneOpts) 53 - if err != nil { 54 - return nil, fmt.Errorf("clone failed: %w", err) 55 - } 56 - 57 - ref, err := repo.Head() 58 - if err != nil { 59 - return nil, fmt.Errorf("failed to get HEAD: %w", err) 60 - } 61 - 62 - commit, err := repo.CommitObject(ref.Hash()) 63 - if err != nil { 64 - return nil, fmt.Errorf("failed to get commit: %w", err) 65 - } 66 - 67 - tree, err := commit.Tree() 68 - if err != nil { 69 - return nil, fmt.Errorf("failed to get tree: %w", err) 70 - } 71 - 72 - return &ClonedRepo{ 73 - Repo: repo, 74 - Ref: ref, 75 - Commit: commit, 76 - Tree: tree, 77 - }, nil 78 - } 79 - 80 - func GetFileContentFromTree(tree *object.Tree, path string) ([]byte, error) { 81 - file, err := tree.File(path) 82 - if err != nil { 83 - return nil, fmt.Errorf("file %q not found: %w", path, err) 84 - } 85 - 86 - reader, err := file.Blob.Reader() 87 - if err != nil { 88 - return nil, fmt.Errorf("failed to open reader for %q: %w", path, err) 89 - } 90 - defer reader.Close() 91 - 92 - content, err := io.ReadAll(reader) 93 - if err != nil { 94 - return nil, fmt.Errorf("failed to read content of %q: %w", path, err) 95 - } 96 - 97 - return content, nil 98 - } 99 - 100 - // GetFileContentFromRepo fetches the content of a single file from a Git repository branch. 101 - // It supports optional token-based authentication and performs the clone in-memory. 102 - func GetFileContentFromRepo(opts GitFetchOptions) ([]byte, error) { 103 - if opts.RepoURL == "" || opts.Branch == "" || opts.File == "" { 104 - return nil, fmt.Errorf("missing required fields: repo, branch, or file") 105 - } 106 - 107 - cloneOpts := &git.CloneOptions{ 108 - URL: opts.RepoURL, 109 - SingleBranch: true, 110 - Depth: 1, 111 - ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), 112 - } 113 - 114 - if opts.Token != nil { 115 - cloneOpts.Auth = &gitHttp.BasicAuth{ 116 - Username: "nox", // Username is required by go-git but can be any non-empty string 117 - Password: *opts.Token, 118 - } 119 - } 120 - 121 - repo, err := git.Clone(gitMem.NewStorage(), nil, cloneOpts) 122 - if err != nil { 123 - return nil, fmt.Errorf("clone failed: %w", err) 124 - } 125 - 126 - ref, err := repo.Head() 127 - if err != nil { 128 - return nil, fmt.Errorf("failed to get HEAD: %w", err) 129 - } 130 - 131 - commit, err := repo.CommitObject(ref.Hash()) 132 - if err != nil { 133 - return nil, fmt.Errorf("failed to get commit: %w", err) 134 - } 135 - 136 - tree, err := commit.Tree() 137 - if err != nil { 138 - return nil, fmt.Errorf("failed to get tree: %w", err) 139 - } 140 - 141 - file, err := tree.File(opts.File) 142 - if err != nil { 143 - return nil, fmt.Errorf("file not found: %w", err) 144 - } 145 - 146 - reader, err := file.Blob.Reader() 147 - if err != nil { 148 - return nil, fmt.Errorf("failed to open file reader: %w", err) 149 - } 150 - defer reader.Close() 151 - 152 - content, err := io.ReadAll(reader) 153 - if err != nil { 154 - return nil, fmt.Errorf("failed to read file: %w", err) 155 - } 156 - 157 - return content, nil 158 - }
-10
internal/gitrepo/utils.go
··· 1 - package gitrepo 2 - 3 - import ( 4 - "github.com/go-git/go-git/v5/plumbing/object" 5 - ) 6 - 7 - func FileExistsInTree(tree *object.Tree, path string) bool { 8 - _, err := tree.File(path) 9 - return err == nil 10 - }
+12 -7
internal/processor/gitsync.go
··· 7 7 "github.com/aottr/nox/internal/cache" 8 8 "github.com/aottr/nox/internal/config" 9 9 "github.com/aottr/nox/internal/crypto" 10 - "github.com/aottr/nox/internal/gitrepo" 10 + "github.com/aottr/nox/internal/git" 11 11 "github.com/aottr/nox/internal/state" 12 12 ) 13 13 ··· 21 21 22 22 // retrieve app config and repository 23 23 app := cfg.Apps[*appName] 24 - repoUrl := app.Repo 24 + repoUrl := app.GitConfig.Repo 25 25 if repoUrl == "" { 26 - repoUrl = cfg.DefaultRepo 26 + repoUrl = cfg.GitConfig.Repo 27 + } 28 + branchName := app.GitConfig.Branch 29 + if branchName == "" { 30 + branchName = cfg.GitConfig.Branch 27 31 } 28 - key := cache.RepoKey{Repo: repoUrl, Branch: app.Branch} 32 + 33 + key := cache.RepoKey{Repo: repoUrl, Branch: branchName} 29 34 repo, exists := cache.GlobalCache.Get(key) 30 35 if !exists { 31 36 var err error 32 - repo, err = cache.GlobalCache.FetchRepo(key, nil) 37 + repo, err = cache.GlobalCache.FetchRepo(key) 33 38 if err != nil { 34 39 return fmt.Errorf("failed to fetch repo for app %s: %w", *appName, err) 35 40 } ··· 37 42 38 43 // iterate over files and decrypt 39 44 for _, file := range app.Files { 40 - content, err := gitrepo.GetFileContentFromTree(repo, file.Path) 45 + content, err := git.GetFileContentFromTree(repo, file.Path) 41 46 if err != nil { 42 47 return fmt.Errorf("failed to get file %s: %w", file, err) 43 48 } ··· 62 67 63 68 // skip writing file if dry run is set 64 69 if ctx.DryRun { 65 - ctx.Logger.Printf("❌ dry run, not writing file %s", file.Output) 70 + ctx.Logger.Printf("dry run, not writing file %s", file.Output) 66 71 os.Stdout.Write(plaintext) 67 72 continue 68 73 }
+7 -10
internal/processor/validate.go
··· 7 7 // "strings" 8 8 9 9 "github.com/aottr/nox/internal/config" 10 - "github.com/aottr/nox/internal/gitrepo" 10 + "github.com/aottr/nox/internal/git" 11 11 ) 12 12 13 13 func ValidateConfig(cfg *config.Config) error { ··· 22 22 for appName, app := range cfg.Apps { 23 23 fmt.Printf("✅ Validating app %s\n", appName) 24 24 25 - repoURL := app.Repo 26 - if repoURL == "" { 27 - repoURL = cfg.DefaultRepo 25 + gitConf := app.GitConfig 26 + if !gitConf.IsValid() { 27 + gitConf = cfg.GitConfig 28 28 } 29 29 30 - repo, err := gitrepo.CloneRepoInMemory(gitrepo.GitFetchOptions{ 31 - RepoURL: repoURL, 32 - Branch: app.Branch, 33 - }) 30 + repo, err := git.CloneRepo(gitConf) 34 31 if err != nil { 35 32 return fmt.Errorf("failed to clone for app %s: %w", appName, err) 36 33 } 37 34 38 35 for _, file := range app.Files { 39 - if !gitrepo.FileExistsInTree(repo.Tree, file.Path) { 36 + if !git.FileExistsInTree(repo.Tree, file.Path) { 40 37 return fmt.Errorf("❌ file %s missing in app %s", file, appName) 41 38 } 42 39 fmt.Printf("✔️ Found file %s in repo\n", file) 43 40 } 44 41 } 45 - fmt.Println("✨ All checks passed!") 42 + fmt.Println("all checks passed!") 46 43 return nil 47 44 }