this repo has no description
0
fork

Configure Feed

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

init

Alex Ottr e20d51d5 3091a0d7

+902 -1
+7
.envrc
··· 1 + export DIRENV_WARN_TIMEOUT=20s 2 + 3 + eval "$(devenv direnvrc)" 4 + 5 + # The use_devenv function supports passing flags to the devenv command 6 + # For example: use devenv --impure --option services.postgres.enable:bool true 7 + use devenv
+16
.gitignore
··· 30 30 # Editor/IDE 31 31 # .idea/ 32 32 # .vscode/ 33 + 34 + # Devenv 35 + .devenv* 36 + devenv.local.nix 37 + 38 + # direnv 39 + .direnv 40 + 41 + # pre-commit 42 + .pre-commit-config.yaml 43 + 44 + # build artifacts 45 + nox 46 + keys 47 + secrets 48 + .nox-state.json
+67 -1
README.md
··· 1 - # nox 1 + # nox 2 + 3 + ## Getting Started 4 + 5 + ### Prerequisites 6 + 7 + - Go 1.18+ 8 + 9 + ### Installation 10 + 11 + ```bash 12 + go install github.com/aottr/nox/cmd/nox@latest 13 + ``` 14 + 15 + ## Usage 16 + 17 + ### Generate age key 18 + 19 + ```bash 20 + mkdir -p keys secrets 21 + ``` 22 + 23 + ```bash 24 + age-keygen -o keys/key.txt 25 + ``` 26 + 27 + ### Encrypt secrets 28 + 29 + ```bash 30 + age -r <recipient> -o secrets/prod.env.age secrets/prod.env 31 + ``` 32 + 33 + ### Configure 34 + 35 + Create a `config.yaml` file with the following contents: 36 + 37 + ```yaml 38 + interval: "10m" 39 + ageKeyPath: "keys/key.txt" 40 + statePath: ".nox-state.json" 41 + defaultRepo: git@github.com:ShorkBytes/nox-secrets.git 42 + 43 + apps: 44 + debug: 45 + branch: main 46 + files: 47 + - path: debug/debug.age 48 + output: ./secrets/.env 49 + ``` 50 + 51 + ### Run 52 + 53 + ```bash 54 + nox --help 55 + ``` 56 + 57 + ### Contributing 58 + 59 + Contributions are welcome! 60 + 61 + ```bash 62 + go fmt ./... 63 + ``` 64 + 65 + ## License 66 + 67 + [MIT](LICENSE)
+21
config.yaml
··· 1 + interval: "10m" 2 + ageKeyPath: "keys/key.txt" 3 + statePath: ".nox-state.json" 4 + defaultRepo: git@github.com:ShorkBytes/nox-secrets.git 5 + secrets: 6 + - encrypted: "secrets/prod.env.age" 7 + output: "secrets/prod.env" 8 + 9 + apps: 10 + debug: 11 + branch: main 12 + files: 13 + - path: debug/debug.age 14 + output: ./secrets/.env 15 + 16 + debug2: 17 + branch: main 18 + repo: git@github.com:ShorkBytes/nox-secrets.git 19 + files: 20 + - path: debug/debug.age 21 + output: ./secrets/debug2.env
+103
devenv.lock
··· 1 + { 2 + "nodes": { 3 + "devenv": { 4 + "locked": { 5 + "dir": "src/modules", 6 + "lastModified": 1749934215, 7 + "owner": "cachix", 8 + "repo": "devenv", 9 + "rev": "0ad2d684f722b41578b34670428161d996382e64", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "dir": "src/modules", 14 + "owner": "cachix", 15 + "repo": "devenv", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-compat": { 20 + "flake": false, 21 + "locked": { 22 + "lastModified": 1747046372, 23 + "owner": "edolstra", 24 + "repo": "flake-compat", 25 + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "edolstra", 30 + "repo": "flake-compat", 31 + "type": "github" 32 + } 33 + }, 34 + "git-hooks": { 35 + "inputs": { 36 + "flake-compat": "flake-compat", 37 + "gitignore": "gitignore", 38 + "nixpkgs": [ 39 + "nixpkgs" 40 + ] 41 + }, 42 + "locked": { 43 + "lastModified": 1749636823, 44 + "owner": "cachix", 45 + "repo": "git-hooks.nix", 46 + "rev": "623c56286de5a3193aa38891a6991b28f9bab056", 47 + "type": "github" 48 + }, 49 + "original": { 50 + "owner": "cachix", 51 + "repo": "git-hooks.nix", 52 + "type": "github" 53 + } 54 + }, 55 + "gitignore": { 56 + "inputs": { 57 + "nixpkgs": [ 58 + "git-hooks", 59 + "nixpkgs" 60 + ] 61 + }, 62 + "locked": { 63 + "lastModified": 1709087332, 64 + "owner": "hercules-ci", 65 + "repo": "gitignore.nix", 66 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 67 + "type": "github" 68 + }, 69 + "original": { 70 + "owner": "hercules-ci", 71 + "repo": "gitignore.nix", 72 + "type": "github" 73 + } 74 + }, 75 + "nixpkgs": { 76 + "locked": { 77 + "lastModified": 1746807397, 78 + "owner": "cachix", 79 + "repo": "devenv-nixpkgs", 80 + "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", 81 + "type": "github" 82 + }, 83 + "original": { 84 + "owner": "cachix", 85 + "ref": "rolling", 86 + "repo": "devenv-nixpkgs", 87 + "type": "github" 88 + } 89 + }, 90 + "root": { 91 + "inputs": { 92 + "devenv": "devenv", 93 + "git-hooks": "git-hooks", 94 + "nixpkgs": "nixpkgs", 95 + "pre-commit-hooks": [ 96 + "git-hooks" 97 + ] 98 + } 99 + } 100 + }, 101 + "root": "root", 102 + "version": 7 103 + }
+6
devenv.nix
··· 1 + { pkgs, lib, config, inputs, ... }: 2 + 3 + { 4 + packages = [ pkgs.git ]; 5 + languages.go.enable = true; 6 + }
+15
devenv.yaml
··· 1 + # yaml-language-server: $schema=https://devenv.sh/devenv.schema.json 2 + inputs: 3 + nixpkgs: 4 + url: github:cachix/devenv-nixpkgs/rolling 5 + 6 + # If you're using non-OSS software, you can set allowUnfree to true. 7 + # allowUnfree: true 8 + 9 + # If you're willing to use a package that's vulnerable 10 + # permittedInsecurePackages: 11 + # - "openssl-1.1.1w" 12 + 13 + # If you have more than one devenv you can merge them 14 + #imports: 15 + # - ./backend
+30
go.mod
··· 1 + module github.com/aottr/nox 2 + 3 + go 1.24.2 4 + 5 + require ( 6 + dario.cat/mergo v1.0.0 // indirect 7 + filippo.io/age v1.2.1 // indirect 8 + github.com/Microsoft/go-winio v0.6.2 // indirect 9 + github.com/ProtonMail/go-crypto v1.1.6 // indirect 10 + github.com/cloudflare/circl v1.6.1 // indirect 11 + github.com/cyphar/filepath-securejoin v0.4.1 // indirect 12 + github.com/docker/docker v28.2.2+incompatible // indirect 13 + github.com/emirpasic/gods v1.18.1 // indirect 14 + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 15 + github.com/go-git/go-billy/v5 v5.6.2 // indirect 16 + github.com/go-git/go-git/v5 v5.16.2 // indirect 17 + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 18 + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 19 + github.com/kevinburke/ssh_config v1.2.0 // indirect 20 + github.com/pjbgf/sha1cd v0.3.2 // indirect 21 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 22 + github.com/skeema/knownhosts v1.3.1 // indirect 23 + github.com/urfave/cli/v3 v3.3.8 // indirect 24 + github.com/xanzy/ssh-agent v0.3.3 // indirect 25 + golang.org/x/crypto v0.37.0 // indirect 26 + golang.org/x/net v0.39.0 // indirect 27 + golang.org/x/sys v0.32.0 // indirect 28 + gopkg.in/warnings.v0 v0.1.2 // indirect 29 + gopkg.in/yaml.v3 v3.0.1 // indirect 30 + )
+79
go.sum
··· 1 + dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 + dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 + filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= 4 + filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= 5 + github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 6 + github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 7 + github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 8 + github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= 9 + github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 10 + github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 11 + github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 12 + github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 13 + github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 14 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 + github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= 17 + github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 18 + github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 19 + github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 20 + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 21 + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 22 + github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 23 + github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 24 + github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= 25 + github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= 26 + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 27 + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 28 + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 29 + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 30 + github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 31 + github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 32 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 + github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 36 + github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 37 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 38 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 40 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 41 + github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 42 + github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 43 + github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 44 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 + github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 46 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 47 + github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= 48 + github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 49 + github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 50 + github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 51 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 52 + golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 53 + golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 54 + golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 55 + golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 56 + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 57 + golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 58 + golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 59 + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 + golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 + golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 + golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 66 + golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 67 + golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 68 + golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 69 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 70 + golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 71 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 + gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 75 + gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 76 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 77 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 78 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 79 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+46
internal/config/config.go
··· 1 + package config 2 + 3 + import ( 4 + "os" 5 + 6 + "gopkg.in/yaml.v3" 7 + ) 8 + 9 + type SecretMapping struct { 10 + EncryptedPath string `yaml:"encrypted"` 11 + OutputPath string `yaml:"output"` 12 + } 13 + 14 + type FileConfig struct { 15 + Path string `yaml:"path"` 16 + Output string `yaml:"output,omitempty"` 17 + } 18 + 19 + type AppConfig struct { 20 + Repo string `yaml:"repo,omitempty"` 21 + Branch string `yaml:"branch"` 22 + Files []FileConfig `yaml:"files"` 23 + } 24 + 25 + type Config struct { 26 + Interval string `yaml:"interval"` 27 + AgeKeyPath string `yaml:"ageKeyPath"` 28 + StatePath string `yaml:"statePath"` 29 + DefaultRepo string `yaml:"defaultRepo"` 30 + Secrets []SecretMapping `yaml:"secrets"` 31 + Apps map[string]AppConfig `yaml:"apps"` 32 + } 33 + 34 + func Load(path string) (*Config, error) { 35 + data, err := os.ReadFile(path) 36 + if err != nil { 37 + return nil, err 38 + } 39 + 40 + var cfg Config 41 + if err := yaml.Unmarshal(data, &cfg); err != nil { 42 + return nil, err 43 + } 44 + 45 + return &cfg, nil 46 + }
+6
internal/constants/constants.go
··· 1 + package constants 2 + 3 + const ( 4 + DefaultStatePath = ".nox-state.json" 5 + DefaultConfigPath = "config.yaml" 6 + )
+68
internal/crypto/decrypt.go
··· 1 + package crypto 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "io" 7 + "os" 8 + 9 + "filippo.io/age" 10 + ) 11 + 12 + func DecryptAgeFile(inputPath, outputPath, identityPath string) error { 13 + identityFile, err := os.ReadFile(identityPath) 14 + if err != nil { 15 + return fmt.Errorf("failed to read identity: %w", err) 16 + } 17 + 18 + identities, err := age.ParseIdentities(bytes.NewReader(identityFile)) 19 + if err != nil { 20 + return fmt.Errorf("failed to parse identities: %w", err) 21 + } 22 + 23 + in, err := os.Open(inputPath) 24 + if err != nil { 25 + return fmt.Errorf("failed to open encrypted file: %w", err) 26 + } 27 + defer in.Close() 28 + 29 + r, err := age.Decrypt(in, identities...) 30 + if err != nil { 31 + return fmt.Errorf("failed to decrypt: %w", err) 32 + } 33 + 34 + out, err := os.Create(outputPath) 35 + if err != nil { 36 + return fmt.Errorf("failed to create output: %w", err) 37 + } 38 + defer out.Close() 39 + 40 + if _, err := io.Copy(out, r); err != nil { 41 + return fmt.Errorf("failed to write: %w", err) 42 + } 43 + 44 + return nil 45 + } 46 + 47 + func DecryptFile(inputPath string, identities []age.Identity) ([]byte, error) { 48 + data, err := os.ReadFile(inputPath) 49 + if err != nil { 50 + return nil, fmt.Errorf("failed to read encrypted file: %w", err) 51 + } 52 + return DecryptBytes(data, identities) 53 + } 54 + 55 + func DecryptBytes(encrypted []byte, identities []age.Identity) ([]byte, error) { 56 + 57 + dec, err := age.Decrypt(bytes.NewReader(encrypted), identities...) 58 + if err != nil { 59 + return nil, fmt.Errorf("age decryption failed: %w", err) 60 + } 61 + 62 + var out bytes.Buffer 63 + if _, err := io.Copy(&out, dec); err != nil { 64 + return nil, fmt.Errorf("failed to read decrypted data: %w", err) 65 + } 66 + 67 + return out.Bytes(), nil 68 + }
+24
internal/crypto/identity.go
··· 1 + package crypto 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "os" 7 + 8 + "filippo.io/age" 9 + ) 10 + 11 + // LoadAgeIdentities reads and parses all age identities from the given file 12 + func LoadAgeIdentities(path string) ([]age.Identity, error) { 13 + data, err := os.ReadFile(path) 14 + if err != nil { 15 + return nil, fmt.Errorf("failed to read age key file: %w", err) 16 + } 17 + 18 + identities, err := age.ParseIdentities(bytes.NewReader(data)) 19 + if err != nil { 20 + return nil, fmt.Errorf("invalid age identity file: %w", err) 21 + } 22 + 23 + return identities, nil 24 + }
+162
internal/gitrepo/memfetch.go
··· 1 + package gitrepo 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "log" 7 + "os" 8 + 9 + git "github.com/go-git/go-git/v5" 10 + "github.com/go-git/go-git/v5/plumbing" 11 + "github.com/go-git/go-git/v5/plumbing/object" 12 + gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http" 13 + gitMem "github.com/go-git/go-git/v5/storage/memory" 14 + ) 15 + 16 + type GitFetchOptions struct { 17 + RepoURL string 18 + Branch string 19 + File string 20 + Token *string // optional 21 + } 22 + 23 + type ClonedRepo struct { 24 + Repo *git.Repository 25 + Tree *object.Tree 26 + Ref *plumbing.Reference 27 + Commit *object.Commit 28 + } 29 + 30 + func CloneRepoInMemory(opts GitFetchOptions) (*ClonedRepo, error) { 31 + 32 + log.Printf("🔄 Cloning repo %s (branch: %s)", opts.RepoURL, opts.Branch) 33 + 34 + cloneOpts := &git.CloneOptions{ 35 + URL: opts.RepoURL, 36 + SingleBranch: true, 37 + Depth: 1, 38 + ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), 39 + } 40 + 41 + // check for token authentication 42 + token := opts.Token 43 + if token == nil { 44 + if envToken, exists := os.LookupEnv("GIT_TOKEN"); exists { 45 + token = &envToken 46 + } 47 + } 48 + if token != nil { 49 + log.Println("🔐 Using token authentication") 50 + cloneOpts.Auth = &gitHttp.BasicAuth{ 51 + Username: "nox", 52 + Password: *token, 53 + } 54 + } 55 + 56 + repo, err := git.Clone(gitMem.NewStorage(), nil, cloneOpts) 57 + if err != nil { 58 + return nil, fmt.Errorf("Clone failed: %w", err) 59 + } 60 + 61 + ref, err := repo.Head() 62 + if err != nil { 63 + return nil, fmt.Errorf("Failed to get HEAD: %w", err) 64 + } 65 + 66 + commit, err := repo.CommitObject(ref.Hash()) 67 + if err != nil { 68 + return nil, fmt.Errorf("Failed to get commit: %w", err) 69 + } 70 + 71 + tree, err := commit.Tree() 72 + if err != nil { 73 + return nil, fmt.Errorf("Failed to get tree: %w", err) 74 + } 75 + 76 + return &ClonedRepo{ 77 + Repo: repo, 78 + Ref: ref, 79 + Commit: commit, 80 + Tree: tree, 81 + }, nil 82 + } 83 + 84 + func GetFileContentFromTree(tree *object.Tree, path string) ([]byte, error) { 85 + file, err := tree.File(path) 86 + if err != nil { 87 + return nil, fmt.Errorf("file %q not found: %w", path, err) 88 + } 89 + 90 + reader, err := file.Blob.Reader() 91 + if err != nil { 92 + return nil, fmt.Errorf("failed to open reader for %q: %w", path, err) 93 + } 94 + defer reader.Close() 95 + 96 + content, err := io.ReadAll(reader) 97 + if err != nil { 98 + return nil, fmt.Errorf("failed to read content of %q: %w", path, err) 99 + } 100 + 101 + return content, nil 102 + } 103 + 104 + // GetFileContentFromRepo fetches the content of a single file from a Git repository branch. 105 + // It supports optional token-based authentication and performs the clone in-memory. 106 + func GetFileContentFromRepo(opts GitFetchOptions) ([]byte, error) { 107 + if opts.RepoURL == "" || opts.Branch == "" || opts.File == "" { 108 + return nil, fmt.Errorf("missing required fields: repo, branch, or file") 109 + } 110 + 111 + cloneOpts := &git.CloneOptions{ 112 + URL: opts.RepoURL, 113 + SingleBranch: true, 114 + Depth: 1, 115 + ReferenceName: plumbing.NewBranchReferenceName(opts.Branch), 116 + } 117 + 118 + if opts.Token != nil { 119 + cloneOpts.Auth = &gitHttp.BasicAuth{ 120 + Username: "nox", // Username is required by go-git but can be any non-empty string 121 + Password: *opts.Token, 122 + } 123 + } 124 + 125 + repo, err := git.Clone(gitMem.NewStorage(), nil, cloneOpts) 126 + if err != nil { 127 + return nil, fmt.Errorf("clone failed: %w", err) 128 + } 129 + 130 + ref, err := repo.Head() 131 + if err != nil { 132 + return nil, fmt.Errorf("failed to get HEAD: %w", err) 133 + } 134 + 135 + commit, err := repo.CommitObject(ref.Hash()) 136 + if err != nil { 137 + return nil, fmt.Errorf("failed to get commit: %w", err) 138 + } 139 + 140 + tree, err := commit.Tree() 141 + if err != nil { 142 + return nil, fmt.Errorf("failed to get tree: %w", err) 143 + } 144 + 145 + file, err := tree.File(opts.File) 146 + if err != nil { 147 + return nil, fmt.Errorf("file not found: %w", err) 148 + } 149 + 150 + reader, err := file.Blob.Reader() 151 + if err != nil { 152 + return nil, fmt.Errorf("failed to open file reader: %w", err) 153 + } 154 + defer reader.Close() 155 + 156 + content, err := io.ReadAll(reader) 157 + if err != nil { 158 + return nil, fmt.Errorf("failed to read file: %w", err) 159 + } 160 + 161 + return content, nil 162 + }
+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 + }
+113
internal/process/gitsync.go
··· 1 + package process 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "os" 7 + 8 + "github.com/aottr/nox/internal/config" 9 + "github.com/aottr/nox/internal/crypto" 10 + "github.com/aottr/nox/internal/gitrepo" 11 + "github.com/aottr/nox/internal/state" 12 + "github.com/go-git/go-git/v5/plumbing/object" 13 + ) 14 + 15 + // ProcessApps clones and decrypts configured app secrets efficiently. 16 + func ProcessApps(cfg *config.Config) error { 17 + type repoKey struct { 18 + Repo string 19 + Branch string 20 + } 21 + 22 + clones := map[repoKey]*object.Tree{} 23 + 24 + identities, err := crypto.LoadAgeIdentities(cfg.AgeKeyPath) 25 + if err != nil { 26 + return fmt.Errorf("Failed to load age identities: %w", err) 27 + } 28 + 29 + st, err := state.Load() 30 + if err != nil { 31 + return fmt.Errorf("Failed to load state: %w", err) 32 + } 33 + 34 + for appName, app := range cfg.Apps { 35 + 36 + fmt.Printf("Processing app %s\n", appName) 37 + 38 + repoUrl := app.Repo 39 + if repoUrl == "" { 40 + repoUrl = cfg.DefaultRepo 41 + } 42 + key := repoKey{Repo: repoUrl, Branch: app.Branch} 43 + 44 + clone, ok := clones[key] 45 + if !ok { 46 + repo, err := gitrepo.CloneRepoInMemory(gitrepo.GitFetchOptions{ 47 + RepoURL: repoUrl, 48 + Branch: app.Branch, 49 + }) 50 + if err != nil { 51 + log.Printf("Clone failed for %s/%s: %v", repoUrl, app.Branch, err) 52 + continue 53 + } 54 + clone = repo.Tree 55 + clones[key] = clone 56 + } 57 + 58 + for _, file := range app.Files { 59 + content, err := gitrepo.GetFileContentFromTree(clone, file.Path) 60 + if err != nil { 61 + log.Printf("Failed to get file %s: %v", file, err) 62 + continue 63 + } 64 + 65 + hash := state.HashContent(content) 66 + cacheKey := state.GenerateKey(appName, file.Path) 67 + 68 + if prevHash, ok := st.Data[cacheKey]; ok && prevHash == hash { 69 + log.Printf("File %s is up to date", file.Path) 70 + continue 71 + } 72 + 73 + plaintext, err := crypto.DecryptBytes(content, identities) 74 + if err != nil { 75 + log.Printf("Failed to decrypt file %s: %v", file.Path, err) 76 + continue 77 + } 78 + 79 + outPath := file.Output 80 + // if outPath == "" { 81 + // // Default output filename if none specified, e.g. replace .age with .env 82 + // outPath = filepath.Base(fileCfg.Path) 83 + // if filepath.Ext(outPath) == ".age" { 84 + // outPath = outPath[:len(outPath)-4] + ".env" 85 + // } 86 + // } 87 + 88 + // if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { 89 + // log.Printf("Failed to create directories for %s: %v", outPath, err) 90 + // continue 91 + // } 92 + 93 + if err := os.WriteFile(outPath, plaintext, 0600); err != nil { 94 + log.Printf("Failed to write decrypted file to %s: %v", outPath, err) 95 + continue 96 + } 97 + 98 + log.Printf("decrypted %s for app %s (size: %d bytes)", file, appName, len(plaintext)) 99 + 100 + st.Data[cacheKey] = hash 101 + st.Touch() 102 + 103 + log.Printf("Decrypted file %s", file) 104 + } 105 + 106 + } 107 + 108 + if err := state.Save(st); err != nil { 109 + return fmt.Errorf("Failed to save state: %w", err) 110 + } 111 + 112 + return nil 113 + }
+47
internal/process/validate.go
··· 1 + package process 2 + 3 + import ( 4 + "fmt" 5 + // "os" 6 + // "path/filepath" 7 + // "strings" 8 + 9 + "github.com/aottr/nox/internal/config" 10 + "github.com/aottr/nox/internal/gitrepo" 11 + ) 12 + 13 + func Validate(cfg *config.Config) error { 14 + if cfg.AgeKeyPath == "" { 15 + return fmt.Errorf("age key path is required") 16 + } 17 + 18 + if cfg.StatePath == "" { 19 + fmt.Printf("state path is not set, defaulting to default.\n") 20 + } 21 + 22 + for appName, app := range cfg.Apps { 23 + fmt.Printf("✅ Validating app %s\n", appName) 24 + 25 + repoURL := app.Repo 26 + if repoURL == "" { 27 + repoURL = cfg.DefaultRepo 28 + } 29 + 30 + repo, err := gitrepo.CloneRepoInMemory(gitrepo.GitFetchOptions{ 31 + RepoURL: repoURL, 32 + Branch: app.Branch, 33 + }) 34 + if err != nil { 35 + return fmt.Errorf("failed to clone for app %s: %w", appName, err) 36 + } 37 + 38 + for _, file := range app.Files { 39 + if !gitrepo.FileExistsInTree(repo.Tree, file.Path) { 40 + return fmt.Errorf("❌ file %s missing in app %s", file, appName) 41 + } 42 + fmt.Printf("✔️ Found file %s in repo\n", file) 43 + } 44 + } 45 + fmt.Println("✨ All checks passed!") 46 + return nil 47 + }
+65
internal/state/state.go
··· 1 + package state 2 + 3 + import ( 4 + "encoding/json" 5 + "log" 6 + "os" 7 + "time" 8 + ) 9 + 10 + // State holds metadata for the cache, including 11 + // the last update timestamp and a map of file hashes. 12 + type State struct { 13 + LastUpdated int64 14 + Data map[string]string 15 + } 16 + 17 + var defaultPath = ".nox-state.json" // fallback default 18 + 19 + // SetPath updates the default file path used for saving and loading state 20 + func SetPath(path string) { 21 + defaultPath = path 22 + } 23 + 24 + // Touch updates the LastUpdated timestamp to the current time. 25 + func (s *State) Touch() { 26 + s.LastUpdated = time.Now().Unix() 27 + } 28 + 29 + // Load reads the state from the state file 30 + // Returns an error if the file cannot be read or unmarshaled. 31 + func Load() (*State, error) { 32 + return loadFromFile(defaultPath) 33 + } 34 + 35 + // Save writes the given State to the state file. 36 + // Overwrites any existing state file. 37 + func Save(state *State) error { 38 + return saveToFile(defaultPath, state) 39 + } 40 + 41 + // loadFromFile reads the state JSON from the specified file path. 42 + func loadFromFile(path string) (*State, error) { 43 + data, err := os.ReadFile(path) 44 + if err != nil { 45 + log.Printf("⚠️ No previous state found, starting fresh: %v", err) 46 + return &State{Data: make(map[string]string)}, nil 47 + } 48 + 49 + var state State 50 + if err := json.Unmarshal(data, &state); err != nil { 51 + return nil, err 52 + } 53 + 54 + return &state, nil 55 + } 56 + 57 + // saveToFile writes the State as JSON to the specified file path. 58 + func saveToFile(path string, state *State) error { 59 + data, err := json.Marshal(state) 60 + if err != nil { 61 + return err 62 + } 63 + 64 + return os.WriteFile(path, data, 0644) 65 + }
+17
internal/state/utils.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "crypto/sha256" 6 + "encoding/hex" 7 + ) 8 + 9 + // HashContent returns the SHA256 hash of the given data. 10 + func HashContent(data []byte) string { 11 + sum := sha256.Sum256(data) 12 + return hex.EncodeToString(sum[:]) 13 + } 14 + 15 + func GenerateKey(appName, file string) string { 16 + return fmt.Sprintf("%s:%s", appName, file) 17 + }