├── bin └── .gitkeep ├── testing ├── recipes │ ├── dira │ │ ├── recipea │ │ ├── recipeb │ │ └── recipec │ ├── dirb │ │ ├── reciped │ │ ├── recipee │ │ └── recipef │ ├── dirc │ │ ├── recipea │ │ ├── recipeb │ │ └── recipec │ ├── apache │ │ ├── init.grlx │ │ └── apache.grlx │ ├── independent.grlx │ ├── invalidReq.grlx │ ├── missing.grlx │ ├── userCreation.grlx │ ├── simpletest.grlx │ └── dev.grlx ├── install.sh ├── sprout_a ├── sprout_b ├── sprout_c ├── sprout_d ├── sprout_e ├── sprout_f └── farmer ├── cmd ├── grlx │ ├── util │ │ └── util_test.go │ ├── main.go │ ├── cmd │ │ ├── tail.go │ │ ├── shell.go │ │ ├── recipe.go │ │ ├── version.go │ │ └── test.go │ └── ingredients │ │ ├── test │ │ └── ping.go │ │ └── cmd │ │ └── run.go └── sprout │ ├── include.go │ └── nats.go ├── .dockerignore ├── docs ├── logos │ ├── grlx.jpg │ ├── adatomic.png │ ├── gladhost.png │ ├── google.png │ ├── newleaf.png │ ├── ghsponsor.png │ ├── dendrascience.png │ └── cellpointsystems.png ├── diagrams │ └── grlx-arch-light.png ├── grlx-farmer.service ├── grlx-sprout.service └── notes.md ├── docker ├── goreleaser.farmer.dockerfile ├── goreleaser.sprout.dockerfile ├── farmer.dockerfile ├── sprout.dockerfile └── sprout-debian.dockerfile ├── packaging ├── scripts │ ├── grlx-sprout-deb-postinstall.sh │ ├── grlx-sprout-rpm-postinstall.sh │ ├── grlx-farmer-deb-postinstall.sh │ └── grlx-farmer-rpm-postinstall.sh ├── etc │ ├── grlx-sprout.conf │ └── grlx-farmer.conf ├── alpine │ ├── grlx-farmer.pre-install │ ├── grlx-farmer.initd │ ├── grlx-sprout.initd │ ├── grlx-sprout.post-install │ ├── ATTRIBUTION.md │ └── grlx-farmer.post-install ├── systemd │ ├── grlx-sprout.service │ ├── grlx-sprout-standalone.service │ ├── grlx-farmer.service │ └── grlx-farmer-standalone.service └── aur │ ├── grlx-sprout.install │ ├── grlx-farmer.install │ └── PKGBUILD.tmpl ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── add_my_company.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── govulncheck.yml │ ├── gitleak.yml │ ├── goreleaser.yml │ ├── todos.yml │ ├── snapshot.yml │ ├── go-licenses.yml │ ├── codeql-analysis.yml │ └── release.yml ├── ingredients ├── service │ ├── providers_bsd.go │ └── providers_linux.go ├── test │ ├── test.go │ └── ping.go ├── file │ ├── file_register.go │ ├── file_test_utils.go │ ├── filePrepend.go │ ├── s3 │ │ └── provider.go │ ├── hashers │ │ └── cachehash.go │ ├── fileManaged.go │ ├── fileMissing.go │ ├── fileExists.go │ ├── fileAbsent.go │ ├── http │ │ ├── provider_test.go │ │ └── provider.go │ ├── providers.go │ ├── fileContains_test.go │ ├── fileExists_test.go │ ├── file_test.go │ ├── fileMissing_test.go │ ├── local │ │ └── provider.go │ ├── fileAppend.go │ ├── fileSymlink.go │ └── fileCached.go ├── user │ ├── userExists.go │ ├── userAbsent.go │ └── userPresent.go ├── group │ ├── groupExists.go │ ├── groupAbsent.go │ └── groupPresent.go └── cmd │ └── interactive.go ├── api ├── handlers │ ├── test.go │ ├── version.go │ └── ingredients │ │ ├── cmd │ │ └── run.go │ │ └── test │ │ └── ping.go ├── client │ ├── version.go │ ├── nats.go │ ├── cook.go │ ├── transport.go │ ├── util.go │ └── util_test.go └── middleware.go ├── .gitignore ├── cook ├── errors.go ├── notes.md ├── summary.go ├── rootball │ ├── types.go │ └── print_cycle.go ├── farmercook_test.go └── sproutcook_test.go ├── types ├── parser │ └── example.grlx └── errors.go ├── DEPENDENCIES.md ├── dependencies ├── gopkg.in │ └── yaml.v3 │ │ ├── NOTICE │ │ └── LICENSE ├── github.com │ ├── taigrr │ │ ├── jety │ │ │ └── LICENSE │ │ ├── systemctl │ │ │ └── LICENSE │ │ └── log-socket │ │ │ └── LICENSE │ ├── gogrlx │ │ └── grlx │ │ │ └── v2 │ │ │ └── LICENSE │ ├── lucasb-eyer │ │ └── go-colorful │ │ │ └── LICENSE │ ├── mattn │ │ ├── go-isatty │ │ │ └── LICENSE │ │ ├── go-colorable │ │ │ └── LICENSE │ │ └── go-runewidth │ │ │ └── LICENSE │ ├── fatih │ │ └── color │ │ │ └── LICENSE.md │ ├── muesli │ │ ├── ansi │ │ │ └── LICENSE │ │ ├── roff │ │ │ └── LICENSE │ │ ├── mango │ │ │ └── LICENSE │ │ ├── termenv │ │ │ └── LICENSE │ │ ├── mango-cobra │ │ │ └── LICENSE │ │ ├── mango-pflag │ │ │ └── LICENSE │ │ └── cancelreader │ │ │ └── LICENSE │ ├── rivo │ │ └── uniseg │ │ │ └── LICENSE.txt │ ├── xo │ │ └── terminfo │ │ │ └── LICENSE │ ├── BurntSushi │ │ └── toml │ │ │ └── COPYING │ ├── aymanbagabas │ │ └── go-osc52 │ │ │ └── v2 │ │ │ └── LICENSE │ ├── djherbis │ │ └── atime │ │ │ └── LICENSE │ ├── charmbracelet │ │ ├── x │ │ │ ├── ansi │ │ │ │ └── LICENSE │ │ │ ├── cellbuf │ │ │ │ └── LICENSE │ │ │ ├── term │ │ │ │ └── LICENSE │ │ │ └── exp │ │ │ │ └── charmtone │ │ │ │ └── LICENSE │ │ ├── bubbles │ │ │ └── LICENSE │ │ ├── bubbletea │ │ │ └── LICENSE │ │ ├── fang │ │ │ └── LICENSE.md │ │ ├── lipgloss │ │ │ ├── LICENSE │ │ │ └── v2 │ │ │ │ └── LICENSE │ │ └── colorprofile │ │ │ └── LICENSE │ ├── gorilla │ │ ├── websocket │ │ │ └── LICENSE │ │ └── mux │ │ │ └── LICENSE │ ├── atotto │ │ └── clipboard │ │ │ └── LICENSE │ ├── google │ │ └── uuid │ │ │ └── LICENSE │ ├── nats-io │ │ └── nats-server │ │ │ └── v2 │ │ │ └── internal │ │ │ └── fastrand │ │ │ └── LICENSE │ ├── spf13 │ │ └── pflag │ │ │ └── LICENSE │ └── klauspost │ │ └── compress │ │ └── s2 │ │ └── LICENSE └── golang.org │ └── x │ ├── sys │ └── LICENSE │ ├── text │ └── LICENSE │ ├── crypto │ └── LICENSE │ ├── time │ └── rate │ │ └── LICENSE │ └── sync │ └── errgroup │ └── LICENSE ├── LICENSE ├── CODE_OF_CONDUCT.md ├── CRUSH.md ├── pki └── pki_test.go ├── docker-compose.yml ├── certs └── nkey.go ├── internal └── update │ └── noupdate.go ├── auth └── sign.go ├── props └── props.go ├── SECURITY.md ├── go.mod └── jobs └── jobs.go /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dira/recipea: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dira/recipeb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dira/recipec: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirb/reciped: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirb/recipee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirb/recipef: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirc/recipea: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirc/recipeb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/recipes/dirc/recipec: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cmd/grlx/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | -------------------------------------------------------------------------------- /testing/recipes/apache/init.grlx: -------------------------------------------------------------------------------- 1 | include: 2 | - .apache 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | config.jso* 3 | configs/ 4 | etc/ 5 | docker/ 6 | local/ 7 | -------------------------------------------------------------------------------- /docs/logos/grlx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/grlx.jpg -------------------------------------------------------------------------------- /docs/logos/adatomic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/adatomic.png -------------------------------------------------------------------------------- /docs/logos/gladhost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/gladhost.png -------------------------------------------------------------------------------- /docs/logos/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/google.png -------------------------------------------------------------------------------- /docs/logos/newleaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/newleaf.png -------------------------------------------------------------------------------- /docs/logos/ghsponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/ghsponsor.png -------------------------------------------------------------------------------- /docs/logos/dendrascience.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/dendrascience.png -------------------------------------------------------------------------------- /docker/goreleaser.farmer.dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY grlx-farmer* /farmer 3 | ENTRYPOINT ["/farmer"] -------------------------------------------------------------------------------- /docs/diagrams/grlx-arch-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/diagrams/grlx-arch-light.png -------------------------------------------------------------------------------- /docs/logos/cellpointsystems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogrlx/grlx/HEAD/docs/logos/cellpointsystems.png -------------------------------------------------------------------------------- /testing/recipes/independent.grlx: -------------------------------------------------------------------------------- 1 | steps: 2 | touch unimportant file: 3 | file.exists: 4 | - name: /tmp/exists 5 | -------------------------------------------------------------------------------- /docker/goreleaser.sprout.dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox 2 | COPY grlx-sprout* /usr/bin/grlx-sprout 3 | ENTRYPOINT ["/usr/bin/grlx-sprout"] -------------------------------------------------------------------------------- /packaging/scripts/grlx-sprout-deb-postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Enable and start service 3 | systemctl daemon-reload 4 | systemctl enable grlx-sprout.service -------------------------------------------------------------------------------- /packaging/scripts/grlx-sprout-rpm-postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Enable and start service 3 | systemctl daemon-reload 4 | systemctl enable grlx-sprout.service -------------------------------------------------------------------------------- /packaging/etc/grlx-sprout.conf: -------------------------------------------------------------------------------- 1 | farmerinterface: 2 | farmerbusport: 5406 3 | farmeripoprt: 5405 4 | farmerurl: https://:5405 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: taigrr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | -------------------------------------------------------------------------------- /ingredients/service/providers_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || netbsd || openbsd || dragonfly 2 | 3 | package service 4 | 5 | // TODO: implement the service provider for BSD 6 | -------------------------------------------------------------------------------- /packaging/alpine/grlx-farmer.pre-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | addgroup -S farmer 2>/dev/null 4 | adduser -S -D -H -s /sbin/nologin -G farmer -g farmer farmer 2>/dev/null 5 | 6 | exit 0 7 | -------------------------------------------------------------------------------- /ingredients/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import nats "github.com/nats-io/nats.go" 4 | 5 | var nc *nats.Conn 6 | 7 | func RegisterNatsConn(n *nats.Conn) { 8 | nc = n 9 | } 10 | -------------------------------------------------------------------------------- /api/handlers/test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import nats "github.com/nats-io/nats.go" 4 | 5 | var conn *nats.Conn 6 | 7 | func RegisterNatsConn(n *nats.Conn) { 8 | conn = n 9 | } 10 | -------------------------------------------------------------------------------- /packaging/alpine/grlx-farmer.initd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | supervisor=supervise-daemon 3 | description="Grlx Farmer" 4 | command="/usr/bin/grlx-farmer" 5 | 6 | depend() { 7 | need net 8 | } 9 | -------------------------------------------------------------------------------- /packaging/alpine/grlx-sprout.initd: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | supervisor=supervise-daemon 3 | 4 | description="Grlx Sprout" 5 | command="/usr/bin/grlx-sprout" 6 | 7 | depend() { 8 | need net 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | config.jso* 3 | configs/ 4 | /etc 5 | .env 6 | *.zip 7 | coverage.out 8 | local/ 9 | .crush/ 10 | 11 | dist/ 12 | cmd/farmer/farmer 13 | cmd/grlx/grlx 14 | cmd/sprout/sprout 15 | *.log 16 | -------------------------------------------------------------------------------- /testing/recipes/invalidReq.grlx: -------------------------------------------------------------------------------- 1 | include: 2 | - dev 3 | 4 | steps: 5 | this is a step that can never run: 6 | file.manage: 7 | - name: /tmp/nonexistant 8 | - requirements: 9 | - True 10 | - nested: dict 11 | -------------------------------------------------------------------------------- /cook/errors.go: -------------------------------------------------------------------------------- 1 | package cook 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrNoRecipe = errors.New("no recipe") 9 | ErrInvalidFormat = errors.New("invalid recipe format") 10 | ErrDuplicateKey = errors.New("duplicate key in joined maps") 11 | ) 12 | -------------------------------------------------------------------------------- /testing/recipes/missing.grlx: -------------------------------------------------------------------------------- 1 | include: 2 | - dev 3 | steps: 4 | fix golang: 5 | file.missing: 6 | - name: /usr/local/go/fake 7 | - requirements: 8 | - require: 9 | - add go to path 10 | - require_any: add go to path 11 | -------------------------------------------------------------------------------- /testing/recipes/userCreation.grlx: -------------------------------------------------------------------------------- 1 | steps: 2 | create void user: 3 | user.present: 4 | - name: void 5 | - uid: "1000" 6 | - gid: "100" 7 | - home: /home/void 8 | - shell: /usr/bin/zsh 9 | - groups: 10 | - wheel 11 | - docker 12 | -------------------------------------------------------------------------------- /packaging/alpine/grlx-sprout.post-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat </dev/null 2>&1; then 4 | useradd -r -s /bin/false -d /var/cache/grlx/farmer farmer 5 | fi 6 | # Set ownership 7 | chown -R farmer:farmer /etc/grlx/pki/farmer /var/cache/grlx/farmer 8 | # Enable and start service 9 | systemctl daemon-reload 10 | systemctl enable grlx-farmer.service -------------------------------------------------------------------------------- /packaging/scripts/grlx-farmer-rpm-postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create farmer user if it doesn't exist 3 | if ! id -u farmer >/dev/null 2>&1; then 4 | useradd -r -s /bin/false -d /var/cache/grlx/farmer farmer 5 | fi 6 | # Set ownership 7 | chown -R farmer:farmer /etc/grlx/pki/farmer /var/cache/grlx/farmer 8 | # Enable and start service 9 | systemctl daemon-reload 10 | systemctl enable grlx-farmer.service -------------------------------------------------------------------------------- /docker/sprout-debian.dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # Note: this dockerfile must be used from a subfolder 3 | FROM golang:1.24 AS builder 4 | VOLUME /go/src 5 | WORKDIR /app 6 | COPY go.mod . 7 | COPY go.sum . 8 | 9 | RUN go mod download 10 | 11 | ADD . /app 12 | RUN make sprout 13 | 14 | 15 | FROM debian:bookworm-slim 16 | COPY --from=builder /app/bin/sprout sprout 17 | ENTRYPOINT ["/sprout"] 18 | -------------------------------------------------------------------------------- /ingredients/file/file_register.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/gogrlx/grlx/v2/ingredients/file/http" 5 | "github.com/gogrlx/grlx/v2/ingredients/file/local" 6 | "github.com/gogrlx/grlx/v2/types" 7 | ) 8 | 9 | func init() { 10 | provMap = make(map[string]types.FileProvider) 11 | RegisterProvider(http.HTTPFile{}) 12 | // RegisterProvider(s3.S3File{}) 13 | RegisterProvider(local.LocalFile{}) 14 | } 15 | -------------------------------------------------------------------------------- /testing/recipes/apache/apache.grlx: -------------------------------------------------------------------------------- 1 | include: 2 | - dev 3 | steps: 4 | configure http config: 5 | file.managed: 6 | - name: /etc/http/conf/http.conf 7 | - source: grlx://apache/http.conf 8 | - user: root 9 | - group: root 10 | - mode: 644 11 | configure keynav: 12 | file.exists: 13 | - name: /home/tai/.config/keynav.conf 14 | - user: tai 15 | - group: tai 16 | - mode: 644 17 | -------------------------------------------------------------------------------- /packaging/alpine/ATTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Attribution Notice 2 | 3 | The contents of this directory are sourced, with light modification, from the [official Alpine repos](https://gitlab.alpinelinux.org/alpine/aports/-/tree/master/community/grlx). 4 | 5 | Alpine recipies and the aports respository generally are MIT-Licensed. 6 | 7 | The original author of the initd and postinstall scripts found here is [Will Sinatra](https://gitlab.alpinelinux.org/durrendal). 8 | -------------------------------------------------------------------------------- /packaging/alpine/grlx-farmer.post-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat < 8 | farmerpki: /etc/grlx/pki/farmer/ 9 | keyfile: /etc/grlx/pki/farmer/tls-key.pem 10 | nkeyfarmerprivfile: /etc/grlx/pki/farmer/farmer.nkey 11 | nkeyfarmerpubfile: /etc/grlx/pki/farmer/farmer.nkey.pub 12 | organization: 13 | rootca: /etc/grlx/pki/farmer/tls-rootca.pem 14 | rootcapriv: /etc/grlx/pki/farmer/tls-rootca-key.pem 15 | pubkeys: 16 | admin: 17 | - -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | All the dependencies required to build grlx are listed in the [go.mod] file. 4 | 5 | The licenses for each dependency are vendored in the [dependencies] directory, sorted by URL. 6 | This directory is automatically updated on push through GitHub actions. 7 | 8 | Licenses associated with the specific commit you are running can be found by using `grlx version` and referencing the associated commit in the (publicly available) git history. 9 | If you are using an official tagged release, the sources associated are available in the zipped archive attached to the release post. 10 | -------------------------------------------------------------------------------- /cmd/grlx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | 6 | log "github.com/taigrr/log-socket/log" 7 | 8 | "github.com/gogrlx/grlx/v2/cmd/grlx/cmd" 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func init() { 13 | log.SetLogLevel(log.LError) 14 | } 15 | 16 | const DocumentationURL = "https://docs.grlx.dev" 17 | 18 | var ( 19 | GitCommit string 20 | Tag string 21 | ) 22 | 23 | func main() { 24 | defer log.Flush() 25 | cmd.Execute(types.Version{ 26 | Arch: runtime.GOOS, 27 | Compiler: runtime.Version(), 28 | GitCommit: GitCommit, 29 | Tag: Tag, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /dependencies/gopkg.in/yaml.v3/NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2016 Canonical Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /ingredients/test/ping.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/gogrlx/grlx/v2/types" 8 | ) 9 | 10 | // TODO allow selector to be more than an ID 11 | func FPing(target types.KeyManager, ping types.PingPong) (types.PingPong, error) { 12 | topic := "grlx.sprouts." + target.SproutID + ".test.ping" 13 | ping.Ping = true 14 | ping.Pong = false 15 | var pong types.PingPong 16 | b, _ := json.Marshal(ping) 17 | msg, err := nc.Request(topic, b, time.Second*15) 18 | if err != nil { 19 | err = json.Unmarshal(msg.Data, &pong) 20 | } 21 | return pong, err 22 | } 23 | 24 | func SPing(ping types.PingPong) (types.PingPong, error) { 25 | ping.Pong = true 26 | return ping, nil 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: GoReleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Set up Go 22 | uses: actions/setup-go@v4 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v5 26 | with: 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/todos.yml: -------------------------------------------------------------------------------- 1 | name: "Generate Issues from TODOs" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | inputs: 8 | MANUAL_COMMIT_REF: 9 | description: "The SHA of the commit to get the diff for" 10 | required: true 11 | MANUAL_BASE_REF: 12 | description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here" 13 | required: false 14 | 15 | jobs: 16 | build: 17 | runs-on: "ubuntu-latest" 18 | steps: 19 | - uses: "actions/checkout@v3" 20 | - name: "TODO to Issue" 21 | uses: "alstr/todo-to-issue-action@v4" 22 | with: 23 | AUTO_ASSIGN: true 24 | -------------------------------------------------------------------------------- /cook/notes.md: -------------------------------------------------------------------------------- 1 | # Cooking 2 | 1. first, determine the root path and then check to see if a state file exists, or error out 3 | 1. next, refresh sprout properties for each target 4 | 1. next, render the file using golang template rendering, *rendering magic* and then return the rendered bytes/string 5 | 1. next, check to ensure a unique state namespace for safe requisite inclusions 6 | 1. next, build dependency graph 7 | 1. check for cycles 8 | 1. test run all recipies to determine changes 9 | 1. run recipies in order in parallel (goroutines) 10 | 1. collect output and return to server 11 | 12 | 1. output returns job id at the bottom 13 | 1. errors are colorized but also called out at bottom for each target and also bottom of summary 14 | 15 | -------------------------------------------------------------------------------- /dependencies/github.com/taigrr/jety/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 by Tai Groot 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 7 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 8 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 9 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 10 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 11 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 12 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /dependencies/github.com/taigrr/systemctl/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 by Tai Groot 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 7 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 8 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 9 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 10 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 11 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 12 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /dependencies/github.com/taigrr/log-socket/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019-2025 by Tai Groot 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 7 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 8 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 9 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 10 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 11 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 12 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /packaging/systemd/grlx-sprout.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=grlx Sprout - Remote Control Agent 3 | Documentation=https://grlx.dev 4 | After=network.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | User=root 10 | Group=root 11 | ExecStart=/usr/bin/grlx-sprout 12 | ExecReload=/bin/kill -HUP $MAINPID 13 | Restart=always 14 | RestartSec=5 15 | KillMode=mixed 16 | TimeoutStopSec=30 17 | 18 | # Security settings 19 | NoNewPrivileges=false 20 | PrivateTmp=true 21 | ProtectKernelTunables=true 22 | ProtectKernelModules=true 23 | ProtectControlGroups=true 24 | 25 | # Working directory and environment 26 | WorkingDirectory=/var/cache/grlx/sprout 27 | Environment=GRLX_CONFIG=/etc/grlx/sprout 28 | 29 | [Install] 30 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | # State applies 2 | 1. first, determine the root path and then check to see if a state file exists, or error out 3 | 1. next, refresh properties/pillars for each target 4 | 1. next, render the file using golang template rendering, *rendering magic* and then return the rendered bytes/string 5 | 1. next, check to ensure a unique state namespace for safe requisite inclusions 6 | 1. next, build dependency graph 7 | 1. check for cycles 8 | 1. test run all recipies to determine changes 9 | 1. run recipies in order in parallel (workgroups/goroutines?) 10 | 1. collect output and return to server 11 | 12 | 13 | 1. output returns job id at the bottom 14 | 1. errors are colorized but also called out at bottom for each target and also bottom of summary 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /testing/farmer: -------------------------------------------------------------------------------- 1 | certfile: /etc/grlx/pki/farmer/tls-cert.pem 2 | certhosts: 3 | - localhost 4 | - 127.0.0.1 5 | - farmer 6 | - grlx 7 | - 192.168.2.4 8 | certificatevalidtime: 8760h0m0s 9 | configroot: /etc/grlx/ 10 | farmerapiport: "5405" 11 | farmerbusinterface: "" 12 | farmerbusport: "5406" 13 | farmerinterface: farmer 14 | farmerpki: /etc/grlx/pki/farmer/ 15 | farmerurl: https://farmer:5405 16 | keyfile: /etc/grlx/pki/farmer/tls-key.pem 17 | nkeyfarmerprivfile: /etc/grlx/pki/farmer/farmer.nkey 18 | nkeyfarmerpubfile: /etc/grlx/pki/farmer/farmer.nkey.pub 19 | organization: GRLX Development 20 | pubkeys: 21 | admin: 22 | - PLACEHOLDER 23 | rootca: /etc/grlx/pki/farmer/tls-rootca.pem 24 | rootcapriv: /etc/grlx/pki/farmer/tls-rootca-key.pem 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-2023 by Tai Groot 2 | Copyright (C) 2024 by the grlx contributors (see https://github.com/gogrlx/grlx/graphs/contributors). 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /packaging/systemd/grlx-sprout-standalone.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=grlx Sprout - Remote Control Agent (Standalone) 3 | Documentation=https://grlx.dev 4 | After=network.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | User=root 10 | Group=root 11 | ExecStart=/usr/local/bin/grlx-sprout 12 | ExecReload=/bin/kill -HUP $MAINPID 13 | Restart=always 14 | RestartSec=5 15 | KillMode=mixed 16 | TimeoutStopSec=30 17 | 18 | # Less restrictive security for self-updates 19 | NoNewPrivileges=false 20 | PrivateTmp=true 21 | ReadWritePaths=/usr/local/bin 22 | ProtectKernelTunables=true 23 | ProtectKernelModules=true 24 | 25 | # Working directory and environment 26 | WorkingDirectory=/var/cache/grlx/sprout 27 | Environment=GRLX_CONFIG=/etc/grlx/sprout 28 | 29 | [Install] 30 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /cook/summary.go: -------------------------------------------------------------------------------- 1 | package cook 2 | 3 | import "github.com/gogrlx/grlx/v2/types" 4 | 5 | func SummarizeSteps(steps []types.SproutStepCompletion) map[string]types.Summary { 6 | summary := make(map[string]types.Summary) 7 | for _, step := range steps { 8 | if _, ok := summary[step.SproutID]; !ok { 9 | summary[step.SproutID] = types.Summary{} 10 | } 11 | stepSummary := summary[step.SproutID] 12 | if step.CompletedStep.ChangesMade { 13 | stepSummary.Changes += 1 14 | } 15 | switch step.CompletedStep.CompletionStatus { 16 | case types.StepCompleted: 17 | stepSummary.Succeeded += 1 18 | case types.StepFailed: 19 | stepSummary.Failures += 1 20 | stepSummary.Errors = append(stepSummary.Errors, step.CompletedStep.Error) 21 | } 22 | summary[step.SproutID] = stepSummary 23 | } 24 | 25 | return summary 26 | } 27 | -------------------------------------------------------------------------------- /cook/rootball/types.go: -------------------------------------------------------------------------------- 1 | package rootball 2 | 3 | import "github.com/gogrlx/grlx/v2/types" 4 | 5 | type RecipeFile struct { 6 | Steps []*types.Step 7 | Includes []string 8 | includes []*RecipeFile 9 | IsIncluded bool 10 | ID string 11 | } 12 | 13 | /* 14 | type Recipe struct { 15 | ID string 16 | Ingredients []*Ingredient 17 | } 18 | 19 | type Ingredient struct { 20 | Dependencies []string 21 | dependencies []*Ingredient 22 | dependents []*Ingredient 23 | isRequisite bool 24 | ID string 25 | } 26 | */ 27 | /* 28 | import: 29 | - dira.recipeb 30 | - dirb.reciped 31 | - dirc.recipeb 32 | 33 | manage-all-files: 34 | cmd.run: 35 | name: ls 36 | args: 37 | - -sl 38 | paht: // this line is misspelled, but not ignored, instead causes a compilation failure 39 | - '/usr/bin/failure/' 40 | */ 41 | -------------------------------------------------------------------------------- /dependencies/github.com/gogrlx/grlx/v2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-2023 by Tai Groot 2 | Copyright (C) 2024 by the grlx contributors (see https://github.com/gogrlx/grlx/graphs/contributors). 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /packaging/aur/grlx-sprout.install: -------------------------------------------------------------------------------- 1 | post_install() { 2 | echo "Reloading systemd daemon..." 3 | systemctl daemon-reload 4 | 5 | echo "grlx-sprout installed. To start the service:" 6 | echo " sudo systemctl enable grlx-sprout.service" 7 | echo " sudo systemctl start grlx-sprout.service" 8 | echo "" 9 | echo "Don't forget to configure /etc/grlx/sprout before starting!" 10 | } 11 | 12 | post_upgrade() { 13 | post_install 14 | } 15 | 16 | pre_remove() { 17 | if systemctl is-active grlx-sprout.service >/dev/null 2>&1; then 18 | echo "Stopping grlx-sprout service..." 19 | systemctl stop grlx-sprout.service 20 | fi 21 | if systemctl is-enabled grlx-sprout.service >/dev/null 2>&1; then 22 | echo "Disabling grlx-sprout service..." 23 | systemctl disable grlx-sprout.service 24 | fi 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug in grlx 4 | title: "[BUG]" 5 | labels: bug, Unlabeled 6 | assignees: taigrr 7 | 8 | --- 9 | 10 | **Please describe the issue.** 11 | [A clear and concise description of what the problem is.] 12 | 13 | **Version Information** 14 | - Commit [ 15 | - Systems 16 | - Farmer OS Distribution: [ ] 17 | - Sprout(s) OS Distribution(s): [] 18 | 19 | **Does this bug exist on the tip of master?** 20 | [ ] Yes 21 | [ ] No 22 | 23 | Please note that bugfixes will only be ported to previous major versions at the request of a Sponsor, and only when applicable and possible. 24 | It is recommended all grlx users run the lastest stable release to take advantage of the newest features and bug fixes. 25 | 26 | **Configuration** 27 | 28 | Please upload the configuration files used if applicable. 29 | -------------------------------------------------------------------------------- /packaging/systemd/grlx-farmer.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=grlx Farmer - Control Plane Service 3 | Documentation=https://grlx.dev 4 | After=network.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | User=farmer 10 | Group=farmer 11 | ExecStart=/usr/bin/grlx-farmer 12 | ExecReload=/bin/kill -HUP $MAINPID 13 | Restart=always 14 | RestartSec=5 15 | KillMode=mixed 16 | TimeoutStopSec=30 17 | 18 | # Security settings 19 | NoNewPrivileges=true 20 | PrivateTmp=true 21 | ProtectSystem=strict 22 | ProtectHome=true 23 | ReadWritePaths=/var/cache/grlx/farmer /etc/grlx/pki/farmer 24 | ProtectKernelTunables=true 25 | ProtectKernelModules=true 26 | ProtectControlGroups=true 27 | 28 | # Working directory and environment 29 | WorkingDirectory=/var/cache/grlx/farmer 30 | Environment=GRLX_CONFIG=/etc/grlx/farmer 31 | 32 | [Install] 33 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /packaging/systemd/grlx-farmer-standalone.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=grlx Farmer - Control Plane Service (Standalone) 3 | Documentation=https://grlx.dev 4 | After=network.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | User=farmer 10 | Group=farmer 11 | ExecStart=/usr/local/bin/grlx-farmer 12 | ExecReload=/bin/kill -HUP $MAINPID 13 | Restart=always 14 | RestartSec=5 15 | KillMode=mixed 16 | TimeoutStopSec=30 17 | 18 | # Less restrictive security for self-updates 19 | NoNewPrivileges=false 20 | PrivateTmp=true 21 | ProtectHome=true 22 | ReadWritePaths=/var/cache/grlx/farmer /etc/grlx/pki/farmer /usr/local/bin 23 | ProtectKernelTunables=true 24 | ProtectKernelModules=true 25 | 26 | # Working directory and environment 27 | WorkingDirectory=/var/cache/grlx/farmer 28 | Environment=GRLX_CONFIG=/etc/grlx/farmer 29 | 30 | [Install] 31 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /ingredients/file/file_test_utils.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gogrlx/grlx/v2/types" 7 | ) 8 | 9 | func compareResults(t *testing.T, result types.Result, expected types.Result) { 10 | if result.Succeeded != expected.Succeeded { 11 | t.Errorf("expected succeeded to be %v, got %v", expected.Succeeded, result.Succeeded) 12 | } 13 | if result.Failed != expected.Failed { 14 | t.Errorf("expected failed to be %v, got %v", expected.Failed, result.Failed) 15 | } 16 | if len(result.Notes) != len(expected.Notes) { 17 | t.Errorf("expected %v notes, got %v. Got %v", len(expected.Notes), len(result.Notes), result.Notes) 18 | return 19 | } 20 | for i, note := range result.Notes { 21 | if note.String() != expected.Notes[i].String() { 22 | t.Errorf("expected note `%v` to be `%s`, got `%s`", i, expected.Notes[i].String(), note.String()) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ingredients/user/userExists.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os/user" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func (u User) exists(ctx context.Context, test bool) (types.Result, error) { 12 | var result types.Result 13 | 14 | userName, ok := u.params["name"].(string) 15 | if !ok { 16 | result.Failed = true 17 | result.Succeeded = false 18 | return result, errors.New("invalid user; name must be a string") 19 | } 20 | if !userExists(userName) { 21 | result.Failed = true 22 | result.Succeeded = false 23 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" does not exist")) 24 | return result, nil 25 | } 26 | result.Failed = false 27 | result.Succeeded = true 28 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" exists")) 29 | return result, nil 30 | } 31 | 32 | func userExists(name string) bool { 33 | _, err := user.Lookup(name) 34 | return err == nil 35 | } 36 | -------------------------------------------------------------------------------- /ingredients/file/filePrepend.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func (f File) prepend(ctx context.Context, test bool) (types.Result, error) { 12 | // TODO 13 | // "name": "string", "text": "[]string", "makedirs": "bool", 14 | // "source": "string", "source_hash": "string", 15 | // "template": "bool", "sources": "[]string", 16 | notes := []fmt.Stringer{} 17 | 18 | name, ok := f.params["name"].(string) 19 | if !ok { 20 | return types.Result{ 21 | Succeeded: false, Failed: true, Notes: notes, 22 | }, types.ErrMissingName 23 | } 24 | name = filepath.Clean(name) 25 | if name == "" { 26 | return types.Result{ 27 | Succeeded: false, Failed: true, Notes: notes, 28 | }, types.ErrMissingName 29 | } 30 | if name == "/" { 31 | return types.Result{ 32 | Succeeded: false, Failed: true, Notes: notes, 33 | }, types.ErrModifyRoot 34 | } 35 | 36 | return f.undef() 37 | } 38 | -------------------------------------------------------------------------------- /ingredients/group/groupExists.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os/user" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func (g Group) exists(ctx context.Context, test bool) (types.Result, error) { 12 | var result types.Result 13 | result.Succeeded = true 14 | result.Failed = false 15 | 16 | groupName, ok := g.params["name"].(string) 17 | if !ok { 18 | result.Failed = true 19 | result.Succeeded = false 20 | return result, errors.New("invalid group; name must be a string") 21 | } 22 | if !groupExists(groupName) { 23 | result.Failed = true 24 | result.Succeeded = false 25 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" does not exist")) 26 | return result, nil 27 | } 28 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" exists")) 29 | return result, nil 30 | } 31 | 32 | func groupExists(name string) bool { 33 | _, err := user.LookupGroup(name) 34 | return err == nil 35 | } 36 | -------------------------------------------------------------------------------- /ingredients/file/s3/provider.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gogrlx/grlx/v2/types" 7 | ) 8 | 9 | type S3File struct { 10 | ID string 11 | Source string 12 | Destination string 13 | Hash string 14 | Props map[string]interface{} 15 | } 16 | 17 | func (sf S3File) Download(context.Context) error { 18 | return nil 19 | } 20 | 21 | func (sf S3File) Properties() (map[string]interface{}, error) { 22 | return sf.Props, nil 23 | } 24 | 25 | func (sf S3File) Parse(id, source, destination, hash string, properties map[string]interface{}) (types.FileProvider, error) { 26 | if properties == nil { 27 | properties = make(map[string]interface{}) 28 | } 29 | return S3File{ID: id, Source: source, Destination: destination, Hash: hash, Props: properties}, nil 30 | } 31 | 32 | func (sf S3File) Protocols() []string { 33 | return []string{"file"} 34 | } 35 | 36 | func (sf S3File) Verify(context.Context) (bool, error) { 37 | return false, nil 38 | } 39 | -------------------------------------------------------------------------------- /api/client/version.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gogrlx/grlx/v2/api" 8 | "github.com/gogrlx/grlx/v2/auth" 9 | "github.com/gogrlx/grlx/v2/config" 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | func GetVersion() (types.Version, error) { 14 | farmerVersion := types.Version{} 15 | FarmerURL := config.FarmerURL 16 | url := FarmerURL + api.Routes["GetVersion"].Pattern // "/test/ping" 17 | req, err := http.NewRequest(http.MethodGet, url, nil) 18 | if err != nil { 19 | return farmerVersion, err 20 | } 21 | req.Header.Set("Content-Type", "application/json") 22 | req.Header.Set("Accept", "application/json") 23 | newToken, err := auth.NewToken() 24 | if err != nil { 25 | return farmerVersion, err 26 | } 27 | req.Header.Set("Authorization", newToken) 28 | resp, err := APIClient.Do(req) 29 | if err != nil { 30 | return farmerVersion, err 31 | } 32 | err = json.NewDecoder(resp.Body).Decode(&farmerVersion) 33 | return farmerVersion, err 34 | } 35 | -------------------------------------------------------------------------------- /ingredients/file/hashers/cachehash.go: -------------------------------------------------------------------------------- 1 | package hashers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | type CacheFile struct { 13 | ID string 14 | Destination string 15 | Hash string 16 | HashType string 17 | } 18 | 19 | func (cf CacheFile) Verify(ctx context.Context) (bool, error) { 20 | f, err := os.Open(cf.Destination) 21 | if err != nil { 22 | if os.IsNotExist(err) { 23 | return false, errors.Join(err, types.ErrFileNotFound) 24 | } 25 | return false, err 26 | } 27 | defer f.Close() 28 | if cf.HashType == "" { 29 | cf.HashType = GuessHashType(cf.Hash) 30 | } 31 | hf, err := GetHashFunc(cf.HashType) 32 | if err != nil { 33 | return false, err 34 | } 35 | hash, matches, err := hf(f, cf.Hash) 36 | if err != nil { 37 | return false, errors.Join(err, types.ErrHashMismatch, fmt.Errorf("recipe step %s: hash for %s failed: expected %s but found %s", cf.ID, cf.Destination, cf.Hash, hash)) 38 | } 39 | return matches, err 40 | } 41 | -------------------------------------------------------------------------------- /api/middleware.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/gogrlx/grlx/v2/auth" 8 | log "github.com/taigrr/log-socket/log" 9 | ) 10 | 11 | func Logger(inner http.Handler, name string) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 13 | start := time.Now() 14 | inner.ServeHTTP(w, r) 15 | 16 | log.Tracef("%s %s %s %s", 17 | r.Method, r.RequestURI, 18 | name, time.Since(start), 19 | ) 20 | }) 21 | } 22 | 23 | func Auth(inner http.Handler, name string) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | switch name { 26 | case "GetCertificate", "PutNKey": 27 | inner.ServeHTTP(w, r) 28 | default: 29 | authToken := r.Header.Get("Authorization") 30 | if authToken == "" { 31 | w.WriteHeader(http.StatusUnauthorized) 32 | return 33 | } 34 | if auth.TokenHasAccess(authToken, r.Method) { 35 | inner.ServeHTTP(w, r) 36 | } else { 37 | w.WriteHeader(http.StatusUnauthorized) 38 | } 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Pledge 4 | 5 | As members, contributors, and leaders, we pledge to make participation in our 6 | community a healthy and professional experience for everyone. 7 | 8 | ## Our Standards 9 | 10 | Examples of behavior that contributes to a positive environment for our 11 | community include: 12 | 13 | * Being respectful of differing opinions, viewpoints, and experiences 14 | * Giving and gracefully accepting constructive feedback 15 | 16 | Examples of unacceptable behavior include: 17 | 18 | * Publishing others' private information without their explicit permission 19 | * Conduct which could reasonably be considered inappropriate in a 20 | professional setting 21 | 22 | ## Scope 23 | 24 | This Code of Conduct applies within all community spaces, and also applies when 25 | an individual is officially representing the community in public spaces. 26 | 27 | ## Enforcement 28 | 29 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 30 | reported to the community leaders responsible for enforcement at 31 | contact@grlx.org. 32 | -------------------------------------------------------------------------------- /dependencies/github.com/lucasb-eyer/go-colorful/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Lucas Beyer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /ingredients/file/fileManaged.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | 7 | "github.com/gogrlx/grlx/v2/types" 8 | ) 9 | 10 | func (f File) managed(ctx context.Context, test bool) (types.Result, error) { 11 | // TODO 12 | // "name": "string", "source": "string", "source_hash": "string", "user": "string", 13 | // "group": "string", "mode": "string", "attrs": "string", "template": "bool", 14 | // "makedirs": "bool", "dir_mode": "string", "replace": "bool", "backup": "string", "show_changes": "bool", 15 | // "create": "bool", 16 | // "follow_symlinks": "bool", "skip_verify": "bool", 17 | 18 | return f.undef() 19 | name, ok := f.params["name"].(string) 20 | if !ok { 21 | return types.Result{ 22 | Succeeded: false, Failed: true, 23 | }, types.ErrMissingName 24 | } 25 | name = filepath.Clean(name) 26 | if name == "" { 27 | return types.Result{ 28 | Succeeded: false, Failed: true, 29 | }, types.ErrMissingName 30 | } 31 | if name == "/" { 32 | return types.Result{ 33 | Succeeded: false, Failed: true, 34 | }, types.ErrModifyRoot 35 | } 36 | return f.undef() 37 | } 38 | -------------------------------------------------------------------------------- /testing/recipes/dev.grlx: -------------------------------------------------------------------------------- 1 | include: 2 | - .missing 3 | - .apache 4 | {{/* if props is_desktop False */}} 5 | steps: 6 | install golang: 7 | archive.extracted: 8 | - name: /usr/local/go 9 | - source: https://go.dev/dl/go1.20.4.linux-amd64.tar.gz 10 | - force: True 11 | - hash: sha256=698ef3243972a51ddb4028e4a1ac63dc6d60821bf18e59a807e051fee0a385bd 12 | - requisites: 13 | - require: configure keynav 14 | add go to path: 15 | file.append: 16 | - name: /etc/profile 17 | - text: | 18 | export PATH=$PATH:/usr/local/go/bin 19 | - requisites: 20 | - require: install golang 21 | 22 | get go version: 23 | cmd.run: 24 | - name: go version 25 | - runas: tai 26 | - require: 27 | - add go to path 28 | temp file deleted: 29 | file.absent: 30 | - name: /tmp/deletable 31 | {{- if (props "is_desktop")}} 32 | configure conky: 33 | file.managed: 34 | - name: /home/tai/.config/conky 35 | - source: grlx://conky.conf 36 | - user: tai 37 | - group: tai 38 | - mode: 644 39 | - makedirs: True 40 | {{ end }} 41 | -------------------------------------------------------------------------------- /dependencies/github.com/mattn/go-isatty/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Yasuhiro MATSUMOTO 2 | 3 | MIT License (Expat) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /cmd/grlx/cmd/tail.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sync" 7 | 8 | "github.com/nats-io/nats.go" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/gogrlx/grlx/v2/api/client" 12 | ) 13 | 14 | var printTex sync.Mutex 15 | 16 | var tailCmd = &cobra.Command{ 17 | Use: "tail", 18 | Short: "Tail the farmer's NATS bus", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | nc, err := client.NewNatsClient() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | sub, err := nc.Subscribe("grlx.>", func(msg *nats.Msg) { 25 | printTex.Lock() 26 | fmt.Println(msg.Subject) 27 | fmt.Println(string(msg.Data)) 28 | printTex.Unlock() 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | sub2, err := nc.Subscribe("_INBOX.>", func(msg *nats.Msg) { 34 | printTex.Lock() 35 | fmt.Println(msg.Subject) 36 | fmt.Println(string(msg.Data)) 37 | printTex.Unlock() 38 | }) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | defer sub.Unsubscribe() 43 | defer sub2.Unsubscribe() 44 | defer nc.Flush() 45 | select {} 46 | }, 47 | } 48 | 49 | func init() { 50 | rootCmd.AddCommand(tailCmd) 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | snapshot: 11 | environment: goreleaser 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | 24 | - name: Import GPG key 25 | id: import_gpg 26 | uses: crazy-max/ghaction-import-gpg@v6 27 | with: 28 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 29 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 30 | 31 | - name: Test GoReleaser 32 | uses: goreleaser/goreleaser-action@v6 33 | with: 34 | distribution: goreleaser-pro 35 | version: "~> v2" 36 | args: release --snapshot --clean --skip=publish 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 40 | -------------------------------------------------------------------------------- /CRUSH.md: -------------------------------------------------------------------------------- 1 | # CRUSH Configuration for grlx 2 | 3 | ## Build Commands 4 | - `make all` - Build all binaries (sprout, grlx, farmer) 5 | - `make sprout` - Build sprout binary 6 | - `make grlx` - Build grlx CLI binary 7 | - `make farmer` - Build farmer binary 8 | - `make clean` - Clean build artifacts 9 | 10 | ## Test Commands 11 | - `go test ./...` - Run all tests 12 | - `go test ./path/to/package` - Run tests for specific package 13 | - `go test -run TestName ./path/to/package` - Run single test 14 | - `go test -v ./...` - Run tests with verbose output 15 | 16 | ## Lint/Format Commands 17 | - `go fmt ./...` - Format all Go code 18 | - `golangci-lint run` - Run linter (if available) 19 | - `go vet ./...` - Run Go vet 20 | 21 | ## Code Style Guidelines 22 | - Use `any` instead of `interface{}` for modern Go 23 | - Package imports: stdlib, third-party, local (with blank lines between groups) 24 | - Error handling: return errors, don't panic in library code 25 | - Naming: camelCase for unexported, PascalCase for exported 26 | - Use context.Context for cancellation and timeouts 27 | - Struct tags: json/yaml tags for serialization 28 | - Test files: `*_test.go` with table-driven tests preferred -------------------------------------------------------------------------------- /cmd/grlx/cmd/shell.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // shellCmd represents the shell command 10 | var shellCmd = &cobra.Command{ 11 | Use: "shell", 12 | Short: "A brief description of your command", 13 | Long: `A longer description that spans multiple lines and likely contains examples 14 | and usage of using your command. For example: 15 | 16 | Cobra is a CLI library for Go that empowers applications. 17 | This application is a tool to generate the needed files 18 | to quickly create a Cobra application.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Println("shell called") 21 | }, 22 | } 23 | 24 | func init() { 25 | // rootCmd.AddCommand(shellCmd) 26 | 27 | // Here you will define your flags and configuration settings. 28 | 29 | // Cobra supports Persistent Flags which will work for this command 30 | // and all subcommands, e.g.: 31 | // shellCmd.PersistentFlags().String("foo", "", "A help for foo") 32 | 33 | // Cobra supports local flags which will only run when this command 34 | // is called directly, e.g.: 35 | // shellCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 36 | } 37 | -------------------------------------------------------------------------------- /dependencies/github.com/fatih/color/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Fatih Arslan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/ansi/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/roff/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/rivo/uniseg/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oliver Kuederle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/xo/terminfo/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anmol Sethi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pki/pki_test.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestIsValidSproutID(t *testing.T) { 9 | testCases := []struct { 10 | id string 11 | shouldSucceed bool 12 | testID string 13 | }{ 14 | {id: "test", shouldSucceed: true, testID: "test"}, 15 | {id: "-test", shouldSucceed: false, testID: "leading hyphen"}, 16 | {id: "te_st", shouldSucceed: true, testID: "embedded underscore"}, 17 | {id: "grlxNode", shouldSucceed: false, testID: "capital letter"}, 18 | {id: "t.est", shouldSucceed: true, testID: "embedded dot"}, 19 | {id: strings.Repeat("a", 300), shouldSucceed: false, testID: "300 long string"}, 20 | {id: strings.Repeat("a", 253), shouldSucceed: true, testID: "253 long string"}, 21 | {id: "0132-465798qwertyuiopasdfghjklzxcv.bnm", shouldSucceed: true, testID: "keyboard smash"}, 22 | {id: "te\nst", shouldSucceed: false, testID: "multiline"}, 23 | } 24 | for _, tc := range testCases { 25 | t.Run(tc.testID, func(t *testing.T) { 26 | if IsValidSproutID(tc.id) != tc.shouldSucceed { 27 | t.Errorf("`%s`: expected %v but got %v", tc.id, tc.shouldSucceed, !tc.shouldSucceed) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dependencies/github.com/BurntSushi/toml/COPYING: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 TOML authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/aymanbagabas/go-osc52/v2/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ayman Bagabas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/djherbis/atime/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dustin H 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/mango/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/termenv/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/grlx/cmd/recipe.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // recipeCmd represents the recipe command 10 | var recipeCmd = &cobra.Command{ 11 | Use: "recipe", 12 | Short: "A brief description of your command", 13 | Long: `A longer description that spans multiple lines and likely contains examples 14 | and usage of using your command. For example: 15 | 16 | Cobra is a CLI library for Go that empowers applications. 17 | This application is a tool to generate the needed files 18 | to quickly create a Cobra application.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Println("recipe called") 21 | }, 22 | } 23 | 24 | func init() { 25 | // rootCmd.AddCommand(recipeCmd) 26 | 27 | // Here you will define your flags and configuration settings. 28 | 29 | // Cobra supports Persistent Flags which will work for this command 30 | // and all subcommands, e.g.: 31 | // recipeCmd.PersistentFlags().String("foo", "", "A help for foo") 32 | 33 | // Cobra supports local flags which will only run when this command 34 | // is called directly, e.g.: 35 | // recipeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 36 | } 37 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/x/ansi/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Charmbracelet, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/x/cellbuf/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Charmbracelet, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/x/term/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Charmbracelet, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/mango-cobra/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/mango-pflag/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/bubbles/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/bubbletea/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/fang/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/lipgloss/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/lipgloss/v2/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/x/exp/charmtone/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Charmbracelet, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/mattn/go-colorable/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yasuhiro Matsumoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/mattn/go-runewidth/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yasuhiro Matsumoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/charmbracelet/colorprofile/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Charmbracelet, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dependencies/github.com/muesli/cancelreader/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Erik Geiser and Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /api/client/nats.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "os" 7 | 8 | "github.com/nats-io/nats.go" 9 | "github.com/taigrr/log-socket/log" 10 | 11 | "github.com/gogrlx/grlx/v2/auth" 12 | "github.com/gogrlx/grlx/v2/config" 13 | ) 14 | 15 | func NewNatsClient() (*nats.Conn, error) { 16 | URL := config.FarmerBusURL 17 | pubkey, err := auth.GetPubkey() 18 | if err != nil { 19 | return nil, err 20 | } 21 | auth.NewToken() 22 | rootCA := config.GrlxRootCA 23 | certPool := x509.NewCertPool() 24 | rootPEM, err := os.ReadFile(rootCA) 25 | if err != nil || rootPEM == nil { 26 | log.Panicf("nats: error loading or parsing rootCA file: %v", err) 27 | } 28 | ok := certPool.AppendCertsFromPEM(rootPEM) 29 | if !ok { 30 | log.Errorf("nats: failed to parse root certificate from %q", rootCA) 31 | } 32 | config := &tls.Config{ 33 | ServerName: config.FarmerInterface, 34 | RootCAs: certPool, 35 | MinVersion: tls.VersionTLS12, 36 | } 37 | 38 | // TODO: add a disconnect handler to reconnect 39 | connOpts := []nats.Option{nats.Name("grlx-cli"), nats.Nkey(pubkey, auth.Sign), nats.Secure(config)} 40 | 41 | log.Tracef("Connecting to %s", URL) 42 | return nats.Connect(URL, connOpts...) 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/go-licenses.yml: -------------------------------------------------------------------------------- 1 | name: "go-licenses" 2 | on: 3 | push: 4 | paths: 5 | - 'go.mod' 6 | branches: 7 | - master 8 | - v1 9 | - v2 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | go-licenses: 15 | runs-on: ubuntu-latest 16 | name: Run go-licenses 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version-file: go.mod 25 | - name: Get google/go-licenses package 26 | run: go install github.com/google/go-licenses@latest 27 | shell: bash 28 | - name: Run go-licenses and save deps 29 | run: | 30 | go-licenses save ./... --save_path=dependencies --force 31 | go-licenses check ./... --disallowed_types="forbidden,restricted" 32 | - name: Push licenses 33 | run: | 34 | git config --global user.name "${{ vars.CI_COMMIT_AUTHOR }}" 35 | git config --global user.email "${{ vars.CI_COMMIT_EMAIL }}" 36 | git add dependencies 37 | git diff --quiet --exit-code --cached dependencies && exit 0 38 | git commit -m "ci(licenses): updated licenses" 39 | git push 40 | -------------------------------------------------------------------------------- /ingredients/file/fileMissing.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | func (f File) missing(ctx context.Context, test bool) (types.Result, error) { 14 | var notes []fmt.Stringer 15 | name, ok := f.params["name"].(string) 16 | if !ok { 17 | return types.Result{ 18 | Succeeded: false, Failed: true, 19 | }, types.ErrMissingName 20 | } 21 | name = filepath.Clean(name) 22 | if name == "" { 23 | return types.Result{ 24 | Succeeded: false, Failed: true, 25 | Changed: false, Notes: nil, 26 | }, types.ErrMissingName 27 | } 28 | _, err := os.Stat(name) 29 | if errors.Is(err, os.ErrNotExist) { 30 | notes = append(notes, types.Snprintf("file `%s` is missing", name)) 31 | return types.Result{ 32 | Succeeded: true, Failed: false, 33 | Changed: false, Notes: notes, 34 | }, nil 35 | } 36 | if err != nil { 37 | return types.Result{ 38 | Succeeded: false, Failed: true, 39 | Changed: false, Notes: notes, 40 | }, err 41 | } 42 | 43 | notes = append(notes, types.Snprintf("file `%s` is not missing", name)) 44 | return types.Result{ 45 | Succeeded: false, Failed: true, 46 | Changed: false, Notes: notes, 47 | }, err 48 | } 49 | -------------------------------------------------------------------------------- /ingredients/file/fileExists.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | func (f File) exists(ctx context.Context, test bool) (types.Result, error) { 14 | var notes []fmt.Stringer 15 | name, ok := f.params["name"].(string) 16 | if !ok { 17 | return types.Result{ 18 | Succeeded: false, Failed: true, 19 | }, types.ErrMissingName 20 | } 21 | name = filepath.Clean(name) 22 | if name == "" { 23 | return types.Result{ 24 | Succeeded: false, Failed: true, 25 | Changed: false, Notes: nil, 26 | }, types.ErrMissingName 27 | } 28 | _, err := os.Stat(name) 29 | if errors.Is(err, os.ErrNotExist) { 30 | notes = append(notes, types.Snprintf("file `%s` does not exist", name)) 31 | return types.Result{ 32 | Succeeded: false, Failed: true, 33 | Changed: false, Notes: notes, 34 | }, nil 35 | } 36 | if err != nil { 37 | return types.Result{ 38 | Succeeded: false, Failed: true, 39 | Changed: false, Notes: notes, 40 | }, err 41 | } 42 | notes = append(notes, types.Snprintf("file `%s` exists", name)) 43 | return types.Result{ 44 | Succeeded: true, 45 | Failed: false, 46 | Changed: false, 47 | Notes: notes, 48 | }, err 49 | } 50 | -------------------------------------------------------------------------------- /packaging/aur/grlx-farmer.install: -------------------------------------------------------------------------------- 1 | post_install() { 2 | echo "Creating farmer user and group..." 3 | if ! getent group farmer >/dev/null 2>&1; then 4 | groupadd -r farmer 5 | fi 6 | if ! getent passwd farmer >/dev/null 2>&1; then 7 | useradd -r -g farmer -d /var/cache/grlx/farmer -s /bin/false farmer 8 | fi 9 | 10 | # Set ownership 11 | chown -R farmer:farmer /etc/grlx/pki/farmer /var/cache/grlx/farmer 2>/dev/null || true 12 | 13 | echo "Reloading systemd daemon..." 14 | systemctl daemon-reload 15 | 16 | echo "grlx-farmer installed. To start the service:" 17 | echo " sudo systemctl enable grlx-farmer.service" 18 | echo " sudo systemctl start grlx-farmer.service" 19 | echo "" 20 | echo "Don't forget to configure /etc/grlx/farmer before starting!" 21 | } 22 | 23 | post_upgrade() { 24 | post_install 25 | } 26 | 27 | pre_remove() { 28 | if systemctl is-active grlx-farmer.service >/dev/null 2>&1; then 29 | echo "Stopping grlx-farmer service..." 30 | systemctl stop grlx-farmer.service 31 | fi 32 | if systemctl is-enabled grlx-farmer.service >/dev/null 2>&1; then 33 | echo "Disabling grlx-farmer service..." 34 | systemctl disable grlx-farmer.service 35 | fi 36 | } -------------------------------------------------------------------------------- /cmd/grlx/ingredients/test/ping.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | 8 | pki "github.com/gogrlx/grlx/v2/api/client" 9 | "github.com/gogrlx/grlx/v2/auth" 10 | "github.com/gogrlx/grlx/v2/config" 11 | . "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | func FPing(target string) (TargetedResults, error) { 15 | FarmerURL := config.FarmerURL 16 | // util target split 17 | // check targets valid 18 | var tr TargetedResults 19 | targets, err := pki.ResolveTargets(target) 20 | if err != nil { 21 | return tr, err 22 | } 23 | var ta TargetedAction 24 | ta.Action = PingPong{} 25 | ta.Target = []KeyManager{} 26 | for _, sprout := range targets { 27 | ta.Target = append(ta.Target, KeyManager{SproutID: sprout}) 28 | } 29 | url := FarmerURL + "/test/ping" 30 | jw, _ := json.Marshal(ta) 31 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jw)) 32 | if err != nil { 33 | return tr, err 34 | } 35 | 36 | req.Header.Set("Content-Type", "application/json") 37 | req.Header.Set("Accept", "application/json") 38 | newToken, err := auth.NewToken() 39 | if err != nil { 40 | return tr, err 41 | } 42 | req.Header.Set("Authorization", newToken) 43 | resp, err := pki.APIClient.Do(req) 44 | if err != nil { 45 | return tr, err 46 | } 47 | err = json.NewDecoder(resp.Body).Decode(&tr) 48 | return tr, err 49 | } 50 | -------------------------------------------------------------------------------- /cmd/grlx/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/gogrlx/grlx/v2/api/client" 11 | "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | // testCmd represents the test command 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Check the cli and farmer versions", 18 | Run: func(cmd *cobra.Command, _ []string) { 19 | grlxVersion := BuildInfo 20 | serverVersion, err := client.GetVersion() 21 | cv := types.CombinedVersion{ 22 | CLI: grlxVersion, 23 | Farmer: serverVersion, 24 | } 25 | if err != nil { 26 | cv.Error = err.Error() 27 | } 28 | switch outputMode { 29 | case "json": 30 | jw, _ := json.Marshal(cv) 31 | fmt.Println(string(jw)) 32 | return 33 | case "": 34 | fallthrough 35 | case "text": 36 | formatter := "%s Version:\n\tTag: %s\n\tCommit: %s\n\tArch: %s\n\tCompiler: %s\n" 37 | fmt.Printf(formatter, "CLI", grlxVersion.Tag, grlxVersion.GitCommit, grlxVersion.Arch, grlxVersion.Compiler) 38 | if err != nil { 39 | log.Println("Error fetching Farmer version: " + err.Error()) 40 | return 41 | } 42 | fmt.Printf(formatter, "Farmer", serverVersion.Tag, serverVersion.GitCommit, serverVersion.Arch, serverVersion.Compiler) 43 | } 44 | }, 45 | } 46 | 47 | func init() { 48 | rootCmd.AddCommand(versionCmd) 49 | } 50 | -------------------------------------------------------------------------------- /dependencies/github.com/gorilla/websocket/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /api/client/cook.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "net/http" 8 | 9 | "github.com/gogrlx/grlx/v2/api" 10 | "github.com/gogrlx/grlx/v2/auth" 11 | "github.com/gogrlx/grlx/v2/config" 12 | "github.com/gogrlx/grlx/v2/types" 13 | ) 14 | 15 | func Cook(target string, cmdCook types.CmdCook) (types.CmdCook, error) { 16 | // util target split 17 | // check targets valid 18 | client := APIClient 19 | ctx := context.Background() 20 | FarmerURL := config.FarmerURL 21 | targets, err := ResolveTargets(target) 22 | if err != nil { 23 | return cmdCook, err 24 | } 25 | var ta types.TargetedAction 26 | ta.Action = cmdCook 27 | ta.Target = []types.KeyManager{} 28 | for _, sprout := range targets { 29 | ta.Target = append(ta.Target, types.KeyManager{SproutID: sprout}) 30 | } 31 | url := FarmerURL + api.Routes["Cook"].Pattern 32 | jw, _ := json.Marshal(ta) 33 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jw)) 34 | if err != nil { 35 | return cmdCook, err 36 | } 37 | req.Header.Set("Content-Type", "application/json") 38 | req.Header.Set("Accept", "application/json") 39 | newToken, err := auth.NewToken() 40 | if err != nil { 41 | return cmdCook, err 42 | } 43 | req.Header.Set("Authorization", newToken) 44 | resp, err := client.Do(req) 45 | if err != nil { 46 | return cmdCook, err 47 | } 48 | err = json.NewDecoder(resp.Body).Decode(&cmdCook) 49 | // TODO connect NATS and start tailing the bus here 50 | return cmdCook, err 51 | } 52 | -------------------------------------------------------------------------------- /cook/rootball/print_cycle.go: -------------------------------------------------------------------------------- 1 | package rootball 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gogrlx/grlx/v2/types" 8 | ) 9 | 10 | func PrintTrees(roots []*types.Step) string { 11 | output := "" 12 | for _, recipe := range roots { 13 | output += printNode(recipe, 0, false) + "\n\n" 14 | } 15 | return output 16 | } 17 | 18 | func printNode(recipe *types.Step, depth int, isLast bool) string { 19 | nodeline := strings.Repeat("|\t", depth) 20 | if depth != 0 { 21 | if isLast { 22 | nodeline += "└── " 23 | } else { 24 | nodeline += "├── " 25 | } 26 | } 27 | nodeline += string(recipe.ID + "\n") 28 | 29 | steps := recipe.Requisites.AllSteps() 30 | for i, step := range steps { 31 | if i == len(steps)-1 { 32 | nodeline += printNode(step, depth+1, true) 33 | } else { 34 | nodeline += printNode(step, depth+1, false) 35 | } 36 | } 37 | return nodeline 38 | } 39 | 40 | func PrintCycle(cycle []types.StepID) string { 41 | out := "" 42 | maxLength := 0 43 | for _, w := range cycle { 44 | if len(w) > maxLength { 45 | maxLength = len(w) 46 | } 47 | } 48 | for i := 0; i < len(cycle); i++ { 49 | switch i { 50 | case 0: 51 | out += fmt.Sprintf("> %s%s V\n", cycle[i], strings.Repeat(" ", maxLength-len(cycle[i]))) 52 | case len(cycle) - 1: 53 | out += fmt.Sprintf("^ %s%s <\n", cycle[i], strings.Repeat(" ", maxLength-len(cycle[i]))) 54 | default: 55 | out += fmt.Sprintf("|| %s%s||\n", cycle[i], strings.Repeat(" ", maxLength-len(cycle[i]))) 56 | } 57 | } 58 | return out 59 | } 60 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | farmer: 4 | container_name: farmer 5 | build: 6 | context: . 7 | dockerfile: docker/farmer.dockerfile 8 | volumes: 9 | - ./testing/farmer:/etc/grlx/farmer 10 | - .:/app 11 | - go-modules:/go/pkg/mod 12 | image: grlx/farmer:latest 13 | restart: unless-stopped 14 | ports: 15 | - "5406:5406" 16 | - "5405:5405" 17 | 18 | sprout_a: 19 | image: grlx/sprout:latest 20 | build: 21 | context: . 22 | dockerfile: docker/sprout.dockerfile 23 | volumes: 24 | - ./testing/sprout_a:/etc/grlx/sprout 25 | - .:/app 26 | - go-modules:/go/pkg/mod 27 | depends_on: 28 | - farmer 29 | sprout_b: 30 | volumes: 31 | - ./testing/sprout_b:/etc/grlx/sprout 32 | image: grlx/sprout:latest 33 | depends_on: 34 | - farmer 35 | sprout_c: 36 | volumes: 37 | - ./testing/sprout_c:/etc/grlx/sprout 38 | image: grlx/sprout:latest 39 | depends_on: 40 | - farmer 41 | sprout_d: 42 | volumes: 43 | - ./testing/sprout_d:/etc/grlx/sprout 44 | image: grlx/sprout:latest 45 | depends_on: 46 | - farmer 47 | sprout_e: 48 | volumes: 49 | - ./testing/sprout_e:/etc/grlx/sprout 50 | image: grlx/sprout:latest 51 | depends_on: 52 | - farmer 53 | sprout_f: 54 | volumes: 55 | - ./testing/sprout_f:/etc/grlx/sprout 56 | image: grlx/sprout:latest 57 | depends_on: 58 | - farmer 59 | volumes: 60 | go-modules: 61 | -------------------------------------------------------------------------------- /api/client/transport.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "os" 11 | "time" 12 | 13 | "github.com/gogrlx/grlx/v2/config" 14 | "github.com/gogrlx/grlx/v2/pki" 15 | "github.com/gogrlx/grlx/v2/types" 16 | ) 17 | 18 | var APIClient *http.Client 19 | 20 | func CreateSecureTransport() error { 21 | APIClient = &http.Client{} 22 | config.LoadConfig("grlx") 23 | err := pki.LoadRootCA("grlx") 24 | if err != nil { 25 | return err 26 | } 27 | RootCA := config.GrlxRootCA 28 | certPool := x509.NewCertPool() 29 | rootPEM, err := os.ReadFile(RootCA) 30 | if err != nil || rootPEM == nil { 31 | return err 32 | } 33 | ok := certPool.AppendCertsFromPEM(rootPEM) 34 | if !ok { 35 | return errors.Join(types.ErrCannotParseRootCA, fmt.Errorf("apiClient: failed to parse root certificate from %q", RootCA)) 36 | } 37 | var apiTransport http.RoundTripper = &http.Transport{ 38 | Proxy: http.ProxyFromEnvironment, 39 | DialContext: (&net.Dialer{ 40 | Timeout: 30 * time.Second, 41 | KeepAlive: 30 * time.Second, 42 | }).DialContext, 43 | ForceAttemptHTTP2: true, 44 | MaxIdleConns: 100, 45 | IdleConnTimeout: 0 * time.Second, 46 | TLSHandshakeTimeout: 10 * time.Second, 47 | ExpectContinueTimeout: 1 * time.Second, 48 | TLSClientConfig: &tls.Config{ 49 | RootCAs: certPool, 50 | MinVersion: tls.VersionTLS12, 51 | }, 52 | } 53 | APIClient.Transport = apiTransport 54 | APIClient.Timeout = time.Second * 10 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /dependencies/golang.org/x/sys/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/golang.org/x/text/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/golang.org/x/crypto/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/golang.org/x/time/rate/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/golang.org/x/sync/errgroup/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /ingredients/file/fileAbsent.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | func (f File) absent(ctx context.Context, test bool) (types.Result, error) { 14 | var notes []fmt.Stringer 15 | err := f.validate() 16 | if err != nil { 17 | return types.Result{ 18 | Succeeded: false, Failed: true, Notes: notes, 19 | }, err 20 | } 21 | name := f.params["name"].(string) 22 | name = filepath.Clean(name) 23 | if name == "/" { 24 | return types.Result{ 25 | Succeeded: false, Failed: true, Notes: notes, 26 | }, types.ErrDeleteRoot 27 | } 28 | _, err = os.Stat(name) 29 | if errors.Is(err, os.ErrNotExist) { 30 | notes = append(notes, types.Snprintf("%v is already absent", name)) 31 | return types.Result{ 32 | Succeeded: true, Failed: false, 33 | Changed: false, Notes: notes, 34 | }, nil 35 | } 36 | if err != nil { 37 | return types.Result{ 38 | Succeeded: false, Failed: true, Notes: notes, 39 | }, err 40 | } 41 | if test { 42 | notes = append(notes, types.Snprintf("%v would be deleted", name)) 43 | return types.Result{ 44 | Succeeded: true, Failed: false, 45 | Changed: true, Notes: notes, 46 | }, nil 47 | } 48 | err = os.Remove(name) 49 | if err != nil { 50 | return types.Result{ 51 | Succeeded: false, Failed: true, Notes: notes, 52 | }, err 53 | } 54 | notes = append(notes, types.Snprintf("%s has been deleted", name)) 55 | return types.Result{ 56 | Succeeded: true, Failed: false, 57 | Changed: true, Notes: notes, 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /dependencies/github.com/atotto/clipboard/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ato Araki. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of @atotto. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/github.com/google/uuid/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009,2014 Google Inc. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/github.com/gorilla/mux/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 The Gorilla Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dependencies/github.com/nats-io/nats-server/v2/internal/fastrand/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 The LevelDB-Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /certs/nkey.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/nats-io/nkeys" 7 | log "github.com/taigrr/log-socket/log" 8 | 9 | "github.com/gogrlx/grlx/v2/config" 10 | ) 11 | 12 | func GetPubNKey(isFarmer bool) (string, error) { 13 | pubFile := config.NKeySproutPubFile 14 | if isFarmer { 15 | pubFile = config.NKeyFarmerPubFile 16 | } 17 | pubKeyBytes, err := os.ReadFile(pubFile) 18 | if err != nil { 19 | return "", err 20 | } 21 | return string(pubKeyBytes), nil 22 | } 23 | 24 | func GenNKey(isFarmer bool) { 25 | privFile := config.NKeySproutPrivFile 26 | pubFile := config.NKeySproutPubFile 27 | if isFarmer { 28 | privFile = config.NKeyFarmerPrivFile 29 | pubFile = config.NKeyFarmerPubFile 30 | } 31 | _, err := os.Stat(privFile) 32 | if err == nil { 33 | return 34 | } 35 | if os.IsNotExist(err) { 36 | kp, err := nkeys.CreateUser() 37 | if err != nil { 38 | log.Panic(err.Error()) 39 | } 40 | pubKey, err := os.OpenFile(pubFile, 41 | os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 42 | 0o600, 43 | ) 44 | if err != nil { 45 | log.Panic(err.Error()) 46 | } 47 | defer pubKey.Close() 48 | key, err := kp.PublicKey() 49 | _, err = pubKey.Write([]byte(key)) 50 | if err != nil { 51 | log.Panic(err.Error()) 52 | } 53 | 54 | privKey, err := os.OpenFile(privFile, 55 | os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 56 | 0o600, 57 | ) 58 | if err != nil { 59 | log.Panic(err.Error()) 60 | } 61 | defer privKey.Close() 62 | pkey, err := kp.Seed() 63 | _, err = privKey.Write(pkey) 64 | if err != nil { 65 | log.Panic(err.Error()) 66 | } 67 | return 68 | } 69 | log.Panic(err) 70 | } 71 | -------------------------------------------------------------------------------- /dependencies/github.com/spf13/pflag/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Alex Ogier. All rights reserved. 2 | Copyright (c) 2012 The Go Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /internal/update/noupdate.go: -------------------------------------------------------------------------------- 1 | //go:build no_self_update 2 | // +build no_self_update 3 | 4 | package selfupdate 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "time" 10 | ) 11 | 12 | // UpdateConfig holds the configuration for self-updates (no-op version) 13 | type UpdateConfig struct { 14 | CurrentVersion string 15 | BinaryName string 16 | UpdateURL string 17 | CheckInterval time.Duration 18 | } 19 | 20 | // Updater handles self-update functionality (no-op version) 21 | type Updater struct { 22 | config UpdateConfig 23 | } 24 | 25 | // ErrSelfUpdateDisabled is returned when self-update is disabled at build time 26 | var ErrSelfUpdateDisabled = errors.New("self-update is disabled in this build") 27 | 28 | // NewUpdater creates a new updater instance (no-op version) 29 | func NewUpdater(config UpdateConfig) *Updater { 30 | return &Updater{ 31 | config: config, 32 | } 33 | } 34 | 35 | // CheckForUpdates always returns that no updates are available 36 | func (u *Updater) CheckForUpdates(ctx context.Context) (string, bool, error) { 37 | return u.config.CurrentVersion, false, ErrSelfUpdateDisabled 38 | } 39 | 40 | // PerformUpdate always returns an error indicating self-update is disabled 41 | func (u *Updater) PerformUpdate(ctx context.Context, version string) error { 42 | return ErrSelfUpdateDisabled 43 | } 44 | 45 | // StartUpdateChecker does nothing in the no-update version 46 | func (u *Updater) StartUpdateChecker(ctx context.Context, callback func(version string, available bool, err error)) { 47 | // No-op: self-update is disabled 48 | if callback != nil { 49 | callback(u.config.CurrentVersion, false, ErrSelfUpdateDisabled) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /dependencies/github.com/klauspost/compress/s2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 The Snappy-Go Authors. All rights reserved. 2 | Copyright (c) 2019 Klaus Post. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /ingredients/file/http/provider_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "path/filepath" 10 | "testing" 11 | ) 12 | 13 | type testServer struct{} 14 | 15 | func (h *testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte("testData")) 17 | } 18 | 19 | func TestDownload(t *testing.T) { 20 | listener, err := net.Listen("tcp", "localhost:0") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | go func() { 26 | v := &testServer{} 27 | http.Serve(listener, v) 28 | }() 29 | port := listener.Addr().(*net.TCPAddr).Port 30 | td := t.TempDir() 31 | type tCase struct { 32 | name string 33 | src string 34 | dst string 35 | hash string 36 | hashType string 37 | err error 38 | ctx context.Context 39 | } 40 | cases := []tCase{{ 41 | name: "test", 42 | src: fmt.Sprintf("http://localhost:%d/test", port), 43 | dst: filepath.Join(td, "dst"), 44 | hash: "3a760fae784d30a1b50e304e97a17355", 45 | err: nil, 46 | ctx: context.Background(), 47 | hashType: "md5", 48 | }} 49 | for _, tc := range cases { 50 | func(tc tCase) { 51 | t.Run(tc.name, func(t *testing.T) { 52 | props := make(map[string]interface{}) 53 | props["hashType"] = tc.hashType 54 | hf, err := (HTTPFile{}).Parse(tc.name, tc.src, tc.dst, tc.hash, props) 55 | if !errors.Is(err, tc.err) { 56 | t.Errorf("want error %v, got %v", tc.err, err) 57 | } 58 | err = hf.Download(tc.ctx) 59 | if !errors.Is(err, tc.err) { 60 | t.Errorf("want error %v, got %v", tc.err, err) 61 | } 62 | }) 63 | }(tc) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cmd/grlx/ingredients/cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | //. "github.com/gogrlx/grlx/v2/config" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "net/http" 9 | 10 | pki "github.com/gogrlx/grlx/v2/api/client" 11 | "github.com/gogrlx/grlx/v2/auth" 12 | "github.com/gogrlx/grlx/v2/config" 13 | "github.com/gogrlx/grlx/v2/types" 14 | ) 15 | 16 | func FRun(target string, command types.CmdRun) (types.TargetedResults, error) { 17 | // util target split 18 | // check targets valid 19 | ctx, cancel := context.WithTimeout(context.Background(), command.Timeout) 20 | defer cancel() 21 | var tr types.TargetedResults 22 | targets, err := pki.ResolveTargets(target) 23 | if err != nil { 24 | return tr, err 25 | } 26 | var ta types.TargetedAction 27 | ta.Action = command 28 | ta.Target = []types.KeyManager{} 29 | for _, sprout := range targets { 30 | ta.Target = append(ta.Target, types.KeyManager{SproutID: sprout}) 31 | } 32 | url := config.FarmerURL + "/cmd/run" 33 | jw, _ := json.Marshal(ta) 34 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jw)) 35 | if err != nil { 36 | return tr, err 37 | } 38 | req.Header.Set("Content-Type", "application/json") 39 | req.Header.Set("Accept", "application/json") 40 | newToken, err := auth.NewToken() 41 | if err != nil { 42 | return tr, err 43 | } 44 | req.Header.Set("Authorization", newToken) 45 | timeoutClient := &http.Client{} 46 | timeoutClient.Timeout = command.Timeout 47 | timeoutClient.Transport = pki.APIClient.Transport 48 | resp, err := timeoutClient.Do(req) 49 | if err != nil { 50 | return tr, err 51 | } 52 | err = json.NewDecoder(resp.Body).Decode(&tr) 53 | return tr, err 54 | } 55 | -------------------------------------------------------------------------------- /ingredients/user/userAbsent.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os/exec" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func (u User) absent(ctx context.Context, test bool) (types.Result, error) { 12 | var result types.Result 13 | result.Succeeded = true 14 | result.Failed = false 15 | userName, ok := u.params["name"].(string) 16 | if !ok { 17 | result.Failed = true 18 | result.Succeeded = false 19 | return result, errors.New("invalid user; name must be a string") 20 | } 21 | if userExists(userName) { 22 | if test { 23 | return types.Result{ 24 | Succeeded: true, 25 | Failed: false, 26 | Notes: append(result.Notes, types.SimpleNote("user "+userName+" would be deleted")), 27 | }, nil 28 | } 29 | cmd := exec.CommandContext(ctx, "userdel", userName) 30 | err := cmd.Run() 31 | if err != nil { 32 | result.Failed = true 33 | result.Succeeded = false 34 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" could not be deleted")) 35 | result.Notes = append(result.Notes, types.SimpleNote(err.Error())) 36 | return result, err 37 | } 38 | if !userExists(userName) { 39 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" deleted")) 40 | return result, nil 41 | } 42 | result.Failed = true 43 | result.Succeeded = false 44 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" could not be deleted")) 45 | return result, errors.New("user " + userName + " could not be deleted") 46 | 47 | } 48 | result.Notes = append(result.Notes, types.SimpleNote("user "+userName+" already absent, nothing to do")) 49 | return result, nil 50 | } 51 | -------------------------------------------------------------------------------- /ingredients/group/groupAbsent.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os/exec" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func (g Group) absent(ctx context.Context, test bool) (types.Result, error) { 12 | var result types.Result 13 | result.Succeeded = true 14 | result.Failed = false 15 | groupName, ok := g.params["name"].(string) 16 | if !ok { 17 | result.Failed = true 18 | result.Succeeded = false 19 | return result, errors.New("invalid group; name must be a string") 20 | } 21 | if groupExists(groupName) { 22 | if test { 23 | return types.Result{ 24 | Succeeded: true, 25 | Failed: false, 26 | Notes: append(result.Notes, types.SimpleNote("group "+groupName+" would be deleted")), 27 | }, nil 28 | } 29 | cmd := exec.CommandContext(ctx, "groupdel", groupName) 30 | err := cmd.Run() 31 | if err != nil { 32 | result.Failed = true 33 | result.Succeeded = false 34 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" could not be deleted")) 35 | result.Notes = append(result.Notes, types.SimpleNote(err.Error())) 36 | return result, err 37 | } 38 | if !groupExists(groupName) { 39 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" deleted")) 40 | return result, nil 41 | } 42 | result.Failed = true 43 | result.Succeeded = false 44 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" could not be deleted")) 45 | return result, errors.New("group " + groupName + " could not be deleted") 46 | 47 | } 48 | result.Notes = append(result.Notes, types.SimpleNote("group "+groupName+" already absent, nothing to do")) 49 | return result, nil 50 | } 51 | -------------------------------------------------------------------------------- /api/client/util.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Target matching behavior is as follows: 9 | // If the target string contains commas, it's considered to be a literal list 10 | // Otherwise, the target string will be treated as a regular expression with 11 | // an implicit '^' prepended and '$' appended. 12 | // To avoid RegExp matching, use a trailing or preceding comma in the target. 13 | func ResolveTargets(target string) ([]string, error) { 14 | validKeys, err := ListKeys() 15 | if err != nil { 16 | return []string{}, err 17 | } 18 | accepted := []string{} 19 | for _, km := range validKeys.Accepted.Sprouts { 20 | accepted = append(accepted, km.SproutID) 21 | } 22 | 23 | if strings.ContainsRune(target, ',') { 24 | targetList := strings.Split(target, ",") 25 | return listIntersection(&targetList, &accepted), nil 26 | } 27 | 28 | return targetRegex(target, &accepted) 29 | } 30 | 31 | func targetRegex(target string, accepted *[]string) ([]string, error) { 32 | if !strings.HasPrefix(target, "^") { 33 | target = "^" + target 34 | } 35 | if !strings.HasSuffix(target, "$") { 36 | target = target + "$" 37 | } 38 | re, err := regexp.Compile(target) 39 | if err != nil { 40 | return []string{}, err 41 | } 42 | matchedTargets := []string{} 43 | for _, x := range *accepted { 44 | if re.MatchString(x) { 45 | matchedTargets = append(matchedTargets, x) 46 | } 47 | } 48 | return matchedTargets, nil 49 | } 50 | 51 | func listIntersection(a *[]string, b *[]string) []string { 52 | hash := make(map[string]bool) 53 | overlap := []string{} 54 | for _, id := range *a { 55 | hash[id] = true 56 | } 57 | for _, id := range *b { 58 | if hash[id] { 59 | overlap = append(overlap, id) 60 | hash[id] = false 61 | } 62 | } 63 | return overlap 64 | } 65 | -------------------------------------------------------------------------------- /packaging/aur/PKGBUILD.tmpl: -------------------------------------------------------------------------------- 1 | # Maintainer: Tai Groot 2 | pkgname='{{ .ProjectName }}' 3 | pkgver='{{ .Version }}' 4 | pkgrel=1 5 | pkgdesc='{{ .Description }}' 6 | arch=('x86_64' 'i686' 'armv7h' 'aarch64') 7 | url='{{ .Homepage }}' 8 | license=('0BSD') 9 | provides=('grlx' 'grlx-farmer' 'grlx-sprout') 10 | conflicts=('grlx-bin' 'grlx-git') 11 | source_x86_64=("${pkgname}-${pkgver}-x86_64.tar.gz::{{ .ArtifactURL }}") 12 | source_i686=("${pkgname}-${pkgver}-i686.tar.gz::{{ .ArtifactURLs.linux_386 }}") 13 | source_armv7h=("${pkgname}-${pkgver}-armv7h.tar.gz::{{ .ArtifactURLs.linux_arm }}") 14 | source_aarch64=("${pkgname}-${pkgver}-aarch64.tar.gz::{{ .ArtifactURLs.linux_arm64 }}") 15 | sha256sums_x86_64=('{{ .ArtifactChecksum }}') 16 | sha256sums_i686=('{{ .ArtifactChecksums.linux_386 }}') 17 | sha256sums_armv7h=('{{ .ArtifactChecksums.linux_arm }}') 18 | sha256sums_aarch64=('{{ .ArtifactChecksums.linux_arm64 }}') 19 | 20 | package() { 21 | # Install binaries 22 | install -Dm755 grlx-${pkgver}-linux-${CARCH} "${pkgdir}/usr/bin/grlx" 23 | install -Dm755 grlx-farmer-${pkgver}-linux-${CARCH} "${pkgdir}/usr/bin/grlx-farmer" 24 | install -Dm755 grlx-sprout-${pkgver}-linux-${CARCH} "${pkgdir}/usr/bin/grlx-sprout" 25 | 26 | # Install systemd service files 27 | install -Dm644 grlx-farmer.service "${pkgdir}/usr/lib/systemd/system/grlx-farmer.service" 28 | install -Dm644 grlx-sprout.service "${pkgdir}/usr/lib/systemd/system/grlx-sprout.service" 29 | 30 | # Install config files 31 | install -Dm644 grlx-farmer.conf "${pkgdir}/etc/grlx/farmer" 32 | install -Dm644 grlx-sprout.conf "${pkgdir}/etc/grlx/sprout" 33 | 34 | # Create directories 35 | install -dm755 "${pkgdir}/etc/grlx/pki/farmer" 36 | install -dm755 "${pkgdir}/var/cache/grlx/farmer" 37 | install -dm755 "${pkgdir}/var/cache/grlx/sprout" 38 | } -------------------------------------------------------------------------------- /types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrAPIRouteNotFound = errors.New("API Route not found") 7 | ErrAlreadyAccepted = errors.New("this Sprout ID was already accepted") 8 | ErrAlreadyDenied = errors.New("this Sprout ID was already denied") 9 | ErrAlreadyRejected = errors.New("this Sprout ID was already rejected") 10 | ErrAlreadyUnaccepted = errors.New("this Sprout ID was already unaccepted") 11 | ErrCannotParseRootCA = errors.New("cannot load the RootCA certificate") 12 | ErrDependencyCycleFound = errors.New("found a dependency cycle") 13 | ErrSproutIDFound = errors.New("a Sprout ID matching that system has already been recorded") 14 | ErrSproutIDInvalid = errors.New("bad user input: invalid SproutID received") 15 | ErrSproutIDNotFound = errors.New("a Sprout ID matching that system cannot be found") 16 | ErrInvalidUserInput = errors.New("invalid user input was received") 17 | 18 | ErrNotImplemented = errors.New("this feature is not yet implemented") 19 | ErrInvalidKeyState = errors.New("code bug: an invalid key state was supplied") 20 | ErrConfirmationLengthIsZero = errors.New("code bug: confirmation options muct not be 0-length") 21 | 22 | ErrInvalidMethod = errors.New("invalid method") 23 | ErrMissingName = errors.New("recipe is missing a name") 24 | ErrMissingSource = errors.New("recipe is missing a source") 25 | ErrMissingHash = errors.New("file is missing a hash") 26 | ErrCacheFailure = errors.New("file caching failed") 27 | ErrMissingContent = errors.New("file is missing content") 28 | 29 | ErrFileNotFound = errors.New("file not found") 30 | ErrHashMismatch = errors.New("file hash mismatch") 31 | ErrDeleteRoot = errors.New("cannot delete root directory") 32 | ErrModifyRoot = errors.New("cannot modify root directory") 33 | ErrMissingTarget = errors.New("target is missing") 34 | ErrPathNotFound = errors.New("path not found") 35 | ) 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 17 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Install Go 42 | uses: actions/setup-go@v4 43 | with: 44 | go-version-file: go.mod 45 | 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v2 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v2 67 | -------------------------------------------------------------------------------- /ingredients/file/providers.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/taigrr/log-socket/log" 10 | 11 | "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | var ( 15 | provTex sync.Mutex 16 | provMap map[string]types.FileProvider 17 | 18 | ErrUnknownProtocol = errors.New("unknown protocol") 19 | ErrUnknownMethod = errors.New("unknown method") 20 | ErrDuplicateProtocol = errors.New("duplicate protocol") 21 | ) 22 | 23 | func RegisterProvider(provider types.FileProvider) error { 24 | provTex.Lock() 25 | defer provTex.Unlock() 26 | var err error 27 | methods := provider.Protocols() 28 | for _, method := range methods { 29 | if method == "" { 30 | err = errors.Join(err, fmt.Errorf("cannot register empty protocol")) 31 | continue 32 | } 33 | // don't override existing protocol handlers 34 | _, ok := provMap[method] 35 | if !ok { 36 | provMap[method] = provider 37 | } else { 38 | err = errors.Join(err, fmt.Errorf("protocol %s already registered", method), ErrDuplicateProtocol) 39 | } 40 | } 41 | return err 42 | } 43 | 44 | func guessProtocol(source string) string { 45 | if strings.HasPrefix(source, "/") { 46 | log.Tracef("guessing protocol %s for source %s", "file", source) 47 | return "file" 48 | } 49 | if strings.Contains(source, "://") { 50 | proto := strings.Split(source, "://")[0] 51 | log.Tracef("guessing protocol %s for source %s", proto, source) 52 | return proto 53 | } 54 | log.Tracef("unknown protocol for source %s", source) 55 | return "" 56 | } 57 | 58 | func NewFileProvider(id string, source, destination, hash string, params map[string]interface{}) (types.FileProvider, error) { 59 | provTex.Lock() 60 | defer provTex.Unlock() 61 | protocol := guessProtocol(source) 62 | r, ok := provMap[protocol] 63 | if !ok { 64 | return nil, errors.Join(ErrUnknownProtocol, fmt.Errorf("unknown protocol: %s", protocol)) 65 | } 66 | return r.Parse(id, source, destination, hash, params) 67 | } 68 | -------------------------------------------------------------------------------- /ingredients/file/fileContains_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/gogrlx/grlx/v2/config" 11 | "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | func TestContains(t *testing.T) { 15 | tempDir := t.TempDir() 16 | existingFile := filepath.Join(tempDir, "there-is-a-file-here") 17 | f, _ := os.Create(existingFile) 18 | defer f.Close() 19 | if _, err := f.WriteString("hello world"); err != nil { 20 | t.Fatal(err) 21 | } 22 | existingFileSrc := filepath.Join(tempDir, "there-is-a-src-here") 23 | f, _ = os.Create(existingFileSrc) 24 | defer f.Close() 25 | if _, err := f.WriteString("hello world"); err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | config.CacheDir = tempDir 30 | defer func() { config.CacheDir = "" }() 31 | 32 | tests := []struct { 33 | name string 34 | params map[string]interface{} 35 | expected types.Result 36 | error error 37 | test bool 38 | }{ 39 | { 40 | name: "IncorrectFilename", 41 | params: map[string]interface{}{ 42 | "name": 1, 43 | }, 44 | expected: types.Result{ 45 | Succeeded: false, 46 | Failed: true, 47 | Notes: []fmt.Stringer{}, 48 | }, 49 | error: types.ErrMissingName, 50 | }, 51 | { 52 | name: "ContainsRoot", 53 | params: map[string]interface{}{ 54 | "name": "/", 55 | }, 56 | expected: types.Result{ 57 | Succeeded: false, 58 | Failed: true, 59 | Notes: []fmt.Stringer{}, 60 | }, 61 | error: types.ErrModifyRoot, 62 | }, 63 | } 64 | for _, test := range tests { 65 | t.Run(test.name, func(t *testing.T) { 66 | f := File{ 67 | id: "", 68 | method: "", 69 | params: test.params, 70 | } 71 | result, _, err := f.contains(context.TODO(), test.test) 72 | if test.error != nil && err.Error() != test.error.Error() { 73 | t.Errorf("expected error %v, got %v", test.error, err) 74 | } 75 | compareResults(t, result, test.expected) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ingredients/service/providers_linux.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sync" 8 | 9 | "github.com/gogrlx/grlx/v2/config" 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | var ( 14 | provTex sync.Mutex 15 | provMap map[string]types.ServiceProvider 16 | 17 | ErrUnknownInit = errors.New("unknown init system") 18 | ErrDuplicateInit = errors.New("provider for init system already initilaized") 19 | Init string 20 | ) 21 | 22 | func init() { 23 | provMap = make(map[string]types.ServiceProvider) 24 | } 25 | 26 | func RegisterProvider(provider types.ServiceProvider) error { 27 | provTex.Lock() 28 | defer provTex.Unlock() 29 | var err error 30 | init := provider.InitName() 31 | if _, ok := provMap[init]; !ok { 32 | provMap[init] = provider 33 | } else { 34 | err = errors.Join(err, fmt.Errorf("protocol %s already registered", init), ErrDuplicateInit) 35 | } 36 | return err 37 | } 38 | 39 | func guessInit() string { 40 | if Init != "" { 41 | return Init 42 | } 43 | // if the init system is specified in the config, use that 44 | if c := config.Init(); c != "" { 45 | Init = c 46 | return c 47 | } 48 | // Check if the init system is systemd 49 | // https://manpages.ubuntu.com/manpages/xenial/en/man3/sd_booted.3.html 50 | if _, ok := os.Stat("/run/systemd/system"); ok == nil { 51 | return "systemd" 52 | } 53 | 54 | for _, initSys := range provMap { 55 | if initSys.IsInit() { 56 | Init = initSys.InitName() 57 | return Init 58 | } 59 | } 60 | // otherwise, return the name of the process in PID 1 61 | f, err := os.ReadFile("/proc/1/comm") 62 | if err != nil { 63 | return "unknown" 64 | } 65 | return string(f) 66 | } 67 | 68 | func NewServiceProvider(id string, method string, params map[string]interface{}) (types.ServiceProvider, error) { 69 | provTex.Lock() 70 | defer provTex.Unlock() 71 | provider, ok := provMap[guessInit()] 72 | if !ok { 73 | return nil, ErrUnknownInit 74 | } 75 | return provider.Parse(id, method, params) 76 | } 77 | -------------------------------------------------------------------------------- /ingredients/file/fileExists_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func TestExists(t *testing.T) { 13 | tempDir := t.TempDir() 14 | fileDNE := filepath.Join(tempDir, "file-does-not-exist") 15 | tests := []struct { 16 | name string 17 | params map[string]interface{} 18 | expected types.Result 19 | error error 20 | test bool 21 | }{ 22 | { 23 | name: "IncorrectFilename", 24 | params: map[string]interface{}{ 25 | "name": 1, 26 | }, 27 | expected: types.Result{ 28 | Succeeded: false, 29 | Failed: true, 30 | Notes: []fmt.Stringer{}, 31 | }, 32 | error: types.ErrMissingName, 33 | }, 34 | { 35 | name: "FileMissing", 36 | params: map[string]interface{}{"name": fileDNE}, 37 | expected: types.Result{ 38 | Succeeded: false, 39 | Failed: true, 40 | Changed: false, 41 | Notes: []fmt.Stringer{types.Snprintf("file `%s` does not exist", fileDNE)}, 42 | }, 43 | error: nil, 44 | }, 45 | { 46 | name: "FileExists", 47 | params: map[string]interface{}{"name": tempDir}, 48 | expected: types.Result{ 49 | Succeeded: true, 50 | Failed: false, 51 | Changed: false, 52 | Notes: []fmt.Stringer{types.Snprintf("file `%s` exists", tempDir)}, 53 | }, 54 | }, 55 | } 56 | for _, test := range tests { 57 | t.Run(test.name, func(t *testing.T) { 58 | f := File{ 59 | id: "", 60 | method: "", 61 | params: test.params, 62 | } 63 | result, err := f.exists(context.TODO(), test.test) 64 | if err != nil || test.error != nil { 65 | if (err == nil && test.error != nil) || (err != nil && test.error == nil) { 66 | t.Errorf("expected error `%v`, got `%v`", test.error, err) 67 | } else if err.Error() != test.error.Error() { 68 | t.Errorf("expected error %v, got %v", test.error, err) 69 | } 70 | } 71 | compareResults(t, result, test.expected) 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ingredients/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/gogrlx/grlx/v2/types" 9 | ) 10 | 11 | func TestRecipeStepUsage(t *testing.T) { 12 | var x types.RecipeCooker 13 | x, err := (File{}).Parse("testFile", "append", map[string]any{ 14 | "name": "testFile", 15 | }) 16 | if err != nil { 17 | t.Error(err) 18 | return 19 | } 20 | res, err := x.Apply(context.Background()) 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | if !res.Succeeded { 26 | t.Errorf("error running %v", x) 27 | } 28 | t.Cleanup(func() { 29 | // remove file 30 | os.Remove("testFile") 31 | }) 32 | } 33 | 34 | func TestDest(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | params map[string]interface{} 38 | out string 39 | error error 40 | }{ 41 | { 42 | name: "TestMissingName", 43 | params: map[string]interface{}{ 44 | "name": "", 45 | }, 46 | out: "", 47 | error: types.ErrMissingName, 48 | }, 49 | { 50 | name: "TestSkipVerify", 51 | params: map[string]interface{}{ 52 | "name": "testFile", 53 | "skip_verify": true, 54 | }, 55 | out: "skip_testFile", 56 | error: nil, 57 | }, 58 | { 59 | name: "TestMissingHash", 60 | params: map[string]interface{}{ 61 | "name": "testFile", 62 | }, 63 | out: "", 64 | error: types.ErrMissingHash, 65 | }, 66 | { 67 | name: "TestMissingHash", 68 | params: map[string]interface{}{ 69 | "name": "testFile", 70 | "hash": "testHash", 71 | }, 72 | out: "testHash", 73 | error: nil, 74 | }, 75 | } 76 | for _, test := range tests { 77 | file := File{ 78 | id: "", 79 | method: "", 80 | params: test.params, 81 | } 82 | t.Run(test.name, func(t *testing.T) { 83 | out, err := file.dest() 84 | if err != test.error { 85 | t.Errorf("expected error %v, got %v", test.error, err) 86 | } 87 | if out != test.out { 88 | t.Errorf("expected %s, got %s", test.out, out) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ingredients/file/fileMissing_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func TestMissing(t *testing.T) { 13 | tempDir := t.TempDir() 14 | fileDNE := filepath.Join(tempDir, "file-does-not-exist") 15 | tests := []struct { 16 | name string 17 | params map[string]interface{} 18 | expected types.Result 19 | error error 20 | test bool 21 | }{ 22 | { 23 | name: "IncorrectFilename", 24 | params: map[string]interface{}{ 25 | "name": 1, 26 | }, 27 | expected: types.Result{ 28 | Succeeded: false, 29 | Failed: true, 30 | Notes: []fmt.Stringer{}, 31 | }, 32 | error: types.ErrMissingName, 33 | }, 34 | { 35 | name: "FileMissing", 36 | params: map[string]interface{}{"name": fileDNE}, 37 | expected: types.Result{ 38 | Succeeded: true, 39 | Failed: false, 40 | Changed: false, 41 | Notes: []fmt.Stringer{types.Snprintf("file `%s` is missing", fileDNE)}, 42 | }, 43 | error: nil, 44 | }, 45 | { 46 | name: "FileExists", 47 | params: map[string]interface{}{"name": tempDir}, 48 | expected: types.Result{ 49 | Succeeded: false, 50 | Failed: true, 51 | Changed: false, 52 | Notes: []fmt.Stringer{types.Snprintf("file `%s` is not missing", tempDir)}, 53 | }, 54 | }, 55 | } 56 | for _, test := range tests { 57 | t.Run(test.name, func(t *testing.T) { 58 | f := File{ 59 | id: "", 60 | method: "", 61 | params: test.params, 62 | } 63 | result, err := f.missing(context.TODO(), test.test) 64 | if err != nil || test.error != nil { 65 | if (err == nil && test.error != nil) || (err != nil && test.error == nil) { 66 | t.Errorf("expected error `%v`, got `%v`", test.error, err) 67 | } else if err.Error() != test.error.Error() { 68 | t.Errorf("expected error %v, got %v", test.error, err) 69 | } 70 | } 71 | compareResults(t, result, test.expected) 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | 7 | permissions: 8 | contents: write 9 | packages: write 10 | 11 | jobs: 12 | goreleaser: 13 | environment: goreleaser 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | 26 | - name: Import GPG key 27 | id: import_gpg 28 | uses: crazy-max/ghaction-import-gpg@v6 29 | with: 30 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 31 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 32 | 33 | - name: Login to Docker Hub 34 | uses: docker/login-action@v3 35 | with: 36 | username: ${{ secrets.DOCKER_USERNAME }} 37 | password: ${{ secrets.DOCKER_PASSWORD }} 38 | 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.actor }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Run GoReleaser 47 | uses: goreleaser/goreleaser-action@v6 48 | with: 49 | distribution: goreleaser-pro 50 | version: "~> v2" 51 | args: release --clean 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 55 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 56 | AWS_REGION: us-east-1 57 | CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} 58 | CLOUDSMITH_REPOSITORY: ${{ secrets.CLOUDSMITH_REPOSITORY }} 59 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 60 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 61 | AUR_KEY: ${{ secrets.AUR_KEY }} 62 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 63 | -------------------------------------------------------------------------------- /ingredients/group/groupPresent.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os/exec" 7 | "os/user" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func (g Group) present(ctx context.Context, test bool) (types.Result, error) { 13 | var result types.Result 14 | 15 | groupName, ok := g.params["name"].(string) 16 | if groupName == "" || !ok { 17 | result.Failed = true 18 | result.Succeeded = false 19 | return result, errors.New("invalid user; name must be a string") 20 | } 21 | args := []string{groupName} 22 | 23 | gid := "" 24 | if gidInter, ok := g.params["gid"]; ok { 25 | gid, ok = gidInter.(string) 26 | } 27 | if gid != "" { 28 | args = append(args, "-g"+gid) 29 | } 30 | 31 | groupByName, err := user.LookupGroup(groupName) 32 | if err != nil { 33 | cmd := exec.CommandContext(ctx, "groupadd", args...) 34 | if test { 35 | result.Succeeded = true 36 | result.Failed = false 37 | result.Changed = true 38 | result.Notes = append(result.Notes, types.SimpleNote("would have added a group by executing: "+cmd.String())) 39 | return result, nil 40 | } 41 | err = cmd.Run() 42 | if err != nil { 43 | result.Failed = true 44 | result.Succeeded = false 45 | return result, err 46 | } 47 | result.Changed = true 48 | return result, nil 49 | } 50 | if groupByName == nil || groupByName.Gid != gid { 51 | cmd := exec.CommandContext(ctx, "groupmod", args...) 52 | if test { 53 | result.Succeeded = true 54 | result.Failed = false 55 | result.Changed = true 56 | result.Notes = append(result.Notes, types.SimpleNote("would have modified the existing group by executing: "+cmd.String())) 57 | return result, nil 58 | } 59 | err = cmd.Run() 60 | if err != nil { 61 | result.Failed = true 62 | result.Succeeded = false 63 | return result, err 64 | } 65 | result.Changed = true 66 | return result, nil 67 | } 68 | result.Succeeded = true 69 | result.Failed = false 70 | result.Changed = false 71 | result.Notes = append(result.Notes, types.SimpleNote("group already exists")) 72 | return result, nil 73 | } 74 | -------------------------------------------------------------------------------- /ingredients/file/local/provider.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "os" 8 | 9 | "github.com/gogrlx/grlx/v2/ingredients/file/hashers" 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | type LocalFile struct { 14 | ID string 15 | Source string 16 | Destination string 17 | Hash string 18 | Props map[string]interface{} 19 | } 20 | 21 | func (lf LocalFile) Download(ctx context.Context) error { 22 | ok, err := lf.Verify(ctx) 23 | // if verification failed because the file doesn't exist, 24 | // that's ok. Otherwise, return the error. 25 | if !errors.Is(err, types.ErrFileNotFound) { 26 | return err 27 | } 28 | // if the file exists and the hash matches, we're done. 29 | if ok { 30 | return nil 31 | } 32 | // otherwise, "download" the file. 33 | f, err := os.Open(lf.Source) 34 | if err != nil { 35 | return err 36 | } 37 | defer f.Close() 38 | dest, err := os.Create(lf.Destination) 39 | if err != nil { 40 | return err 41 | } 42 | _, err = io.Copy(dest, f) 43 | dest.Close() 44 | if err != nil { 45 | return err 46 | } 47 | _, err = lf.Verify(ctx) 48 | return err 49 | } 50 | 51 | func (lf LocalFile) Properties() (map[string]interface{}, error) { 52 | return lf.Props, nil 53 | } 54 | 55 | func (lf LocalFile) Parse(id, source, destination, hash string, properties map[string]interface{}) (types.FileProvider, error) { 56 | if properties == nil { 57 | properties = make(map[string]interface{}) 58 | } 59 | return LocalFile{ID: id, Source: source, Destination: destination, Hash: hash, Props: properties}, nil 60 | } 61 | 62 | func (lf LocalFile) Protocols() []string { 63 | return []string{"file"} 64 | } 65 | 66 | func (lf LocalFile) Verify(ctx context.Context) (bool, error) { 67 | hashType := "" 68 | if lf.Props["hashType"] == nil { 69 | hashType = hashers.GuessHashType(lf.Hash) 70 | } else if ht, ok := lf.Props["hashType"].(string); !ok { 71 | hashType = hashers.GuessHashType(lf.Hash) 72 | } else { 73 | hashType = ht 74 | } 75 | cf := hashers.CacheFile{ 76 | ID: lf.ID, 77 | Destination: lf.Destination, 78 | Hash: lf.Hash, 79 | HashType: hashType, 80 | } 81 | return cf.Verify(ctx) 82 | } 83 | 84 | func init() { 85 | } 86 | -------------------------------------------------------------------------------- /cmd/grlx/cmd/test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/fatih/color" 10 | test "github.com/gogrlx/grlx/v2/cmd/grlx/ingredients/test" 11 | "github.com/gogrlx/grlx/v2/types" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // testCmd represents the test command 16 | var testCmd = &cobra.Command{ 17 | Use: "test", 18 | Short: "Various utilities to monitor and test Sprout connections", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | cmd.Help() 21 | }, 22 | } 23 | 24 | func init() { 25 | testCmdPing.Flags().BoolVarP(&targetAll, "all", "A", false, "Ping all Sprouts") 26 | testCmd.PersistentFlags().StringVarP(&sproutTarget, "target", "T", "", "List of target Sprouts") 27 | testCmd.AddCommand(testCmdPing) 28 | rootCmd.AddCommand(testCmd) 29 | } 30 | 31 | var testCmdPing = &cobra.Command{ 32 | Use: "ping [key id]", 33 | Short: "Determine if a given Sprout is online", 34 | Args: cobra.NoArgs, 35 | Run: func(cmd *cobra.Command, args []string) { 36 | if targetAll { 37 | sproutTarget = ".*" 38 | } 39 | results, err := test.FPing(sproutTarget) 40 | // TODO: output error message in correct outputMode 41 | if err != nil { 42 | switch err { 43 | case types.ErrSproutIDNotFound: 44 | log.Fatalf("A targeted Sprout does not exist or is not accepted..") 45 | default: 46 | log.Panic(err) 47 | } 48 | } 49 | switch outputMode { 50 | case "json": 51 | // TODO: Unmarshall the array specifically instead of the results object 52 | jw, _ := json.Marshal(results) 53 | fmt.Println(string(jw)) 54 | return 55 | case "": 56 | fallthrough 57 | case "text": 58 | for keyID, result := range results.Results { 59 | jw, err := json.Marshal(result) 60 | if err != nil { 61 | color.Red("%s: \n returned an invalid message!\n", keyID) 62 | continue 63 | } 64 | var value types.PingPong 65 | err = json.NewDecoder(bytes.NewBuffer(jw)).Decode(&value) 66 | if err != nil { 67 | color.Red("%s returned an invalid message!\n", keyID) 68 | } 69 | if value.Pong { 70 | fmt.Printf("%s: \"pong!\"\n", keyID) 71 | } else { 72 | color.Red("%s is offline!\n", keyID) 73 | } 74 | } 75 | return 76 | } 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /api/client/util_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_listIntersection(t *testing.T) { 8 | testCases := []struct { 9 | id string 10 | a []string 11 | b []string 12 | res []string 13 | }{ 14 | { 15 | a: []string{"1", "2", "3", "4"}, 16 | b: []string{"1", "2", "4", "4", "5"}, 17 | res: []string{"1", "2", "4"}, 18 | id: "deduplicated overlap", 19 | }, 20 | {a: []string{}, b: []string{}, res: []string{}, id: "all empty"}, 21 | {a: []string{"a"}, b: []string{}, res: []string{}, id: "b empty"}, 22 | {a: []string{}, b: []string{"b"}, res: []string{}, id: "a empty"}, 23 | } 24 | for _, tc := range testCases { 25 | t.Run(tc.id, func(t *testing.T) { 26 | overlap := listIntersection(&tc.a, &tc.b) 27 | if len(overlap) != len(tc.res) { 28 | t.Errorf("Expected and actual overlaps have different lengths.") 29 | } 30 | for i, x := range overlap { 31 | if x != tc.res[i] { 32 | t.Errorf("found %s != %s ", x, tc.res[i]) 33 | } 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func Test_targetRegex(t *testing.T) { 40 | testCases := []struct { 41 | id string 42 | a []string 43 | t string 44 | res []string 45 | }{ 46 | {a: []string{"b", "bc", "abcd", "bcde"}, t: "b", res: []string{"b"}, id: "lazy match"}, 47 | {a: []string{"b", "bc", "abcd", "bcde"}, t: "^b$", res: []string{"b"}, id: "lazy match with ^$"}, 48 | {a: []string{"b", "bc", "abcd", "bcde"}, t: ".*b", res: []string{"b"}, id: "*b"}, 49 | {a: []string{"b", "bc", "abcd", "bcde"}, t: ".*", res: []string{"b", "bc", "abcd", "bcde"}, id: "match all"}, 50 | {a: []string{"b", "bc", "abcd", "bcde"}, t: "", res: []string{}, id: "empty string"}, 51 | {a: []string{"b", "bc", "abcd", "bcde"}, t: "bc+", res: []string{"bc"}, id: "match repeating c's"}, 52 | {a: []string{"b", "bc", "abcd", "bcde"}, t: "bc.*", res: []string{"bc", "bcde"}, id: "match with .*"}, 53 | } 54 | for _, tc := range testCases { 55 | t.Run(tc.id, func(t *testing.T) { 56 | matches, _ := targetRegex(tc.t, &tc.a) 57 | if len(matches) != len(tc.res) { 58 | t.Errorf("Expected and actual matches have different lengths.") 59 | } 60 | for i, x := range matches { 61 | if x != tc.res[i] { 62 | t.Errorf("found %s != %s ", x, tc.res[i]) 63 | } 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dependencies/gopkg.in/yaml.v3/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | This project is covered by two different licenses: MIT and Apache. 3 | 4 | #### MIT License #### 5 | 6 | The following files were ported to Go from C files of libyaml, and thus 7 | are still covered by their original MIT license, with the additional 8 | copyright staring in 2011 when the project was ported over: 9 | 10 | apic.go emitterc.go parserc.go readerc.go scannerc.go 11 | writerc.go yamlh.go yamlprivateh.go 12 | 13 | Copyright (c) 2006-2010 Kirill Simonov 14 | Copyright (c) 2006-2011 Kirill Simonov 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | this software and associated documentation files (the "Software"), to deal in 18 | the Software without restriction, including without limitation the rights to 19 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 20 | of the Software, and to permit persons to whom the Software is furnished to do 21 | so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | 34 | ### Apache License ### 35 | 36 | All the remaining project files are covered by the Apache license: 37 | 38 | Copyright (c) 2011-2019 Canonical Ltd 39 | 40 | Licensed under the Apache License, Version 2.0 (the "License"); 41 | you may not use this file except in compliance with the License. 42 | You may obtain a copy of the License at 43 | 44 | http://www.apache.org/licenses/LICENSE-2.0 45 | 46 | Unless required by applicable law or agreed to in writing, software 47 | distributed under the License is distributed on an "AS IS" BASIS, 48 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 49 | See the License for the specific language governing permissions and 50 | limitations under the License. 51 | -------------------------------------------------------------------------------- /api/handlers/ingredients/cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | 10 | //. "github.com/gogrlx/grlx/v2/config" 11 | "github.com/gogrlx/grlx/v2/ingredients/cmd" 12 | "github.com/gogrlx/grlx/v2/pki" 13 | "github.com/gogrlx/grlx/v2/types" 14 | log "github.com/taigrr/log-socket/log" 15 | ) 16 | 17 | // TODO: add callback event for when new key is PUT to the server 18 | func HCmdRun(w http.ResponseWriter, r *http.Request) { 19 | var targetAction types.TargetedAction 20 | // grab the body of the req 21 | err := json.NewDecoder(r.Body).Decode(&targetAction) 22 | if err != nil { 23 | log.Trace("An invalid request was made.") 24 | http.Error(w, err.Error(), http.StatusBadRequest) 25 | return 26 | } 27 | jw, _ := json.Marshal(targetAction.Action) 28 | var command types.CmdRun 29 | err = json.NewDecoder(bytes.NewBuffer(jw)).Decode(&command) 30 | if err != nil { 31 | log.Trace("An invalid request was made.") 32 | http.Error(w, err.Error(), http.StatusBadRequest) 33 | return 34 | } 35 | // verify our sprout id is valid 36 | for _, target := range targetAction.Target { 37 | if !pki.IsValidSproutID(target.SproutID) || strings.Contains(target.SproutID, "_") { 38 | log.Trace("An invalid Sprout ID was submitted. Ignoring.") 39 | w.WriteHeader(http.StatusBadRequest) 40 | return 41 | } 42 | registered, _ := pki.NKeyExists(target.SproutID, "") 43 | if !registered { 44 | var results types.TargetedResults 45 | results.Results = nil 46 | log.Trace("An unknown Sprout was pinged. Ignoring.") 47 | jw, _ := json.Marshal(results) 48 | w.WriteHeader(http.StatusNotFound) 49 | w.Write(jw) 50 | return 51 | } 52 | } 53 | 54 | var results types.TargetedResults 55 | var wg sync.WaitGroup 56 | var m sync.Mutex 57 | results.Results = make(map[string]interface{}) 58 | for _, target := range targetAction.Target { 59 | wg.Add(1) 60 | 61 | go func(target types.KeyManager) { 62 | defer wg.Done() 63 | result, err := cmd.FRun(target, command) 64 | if err != nil { 65 | log.Tracef("Error running command on the Sprout: %v", err) 66 | } 67 | result.Error = err 68 | m.Lock() 69 | results.Results[target.SproutID] = result 70 | m.Unlock() 71 | }(target) 72 | } 73 | wg.Wait() 74 | jr, _ := json.Marshal(results) 75 | w.WriteHeader(http.StatusOK) 76 | w.Write(jr) 77 | } 78 | -------------------------------------------------------------------------------- /cook/farmercook_test.go: -------------------------------------------------------------------------------- 1 | package cook 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func TestCook(t *testing.T) { 13 | t.Run("apache", func(t *testing.T) { 14 | // err := SendCookEvent("", "apache", "") 15 | // if err != nil { 16 | // t.Error(err) 17 | // } 18 | // fmt.Println(jid) 19 | }) 20 | } 21 | 22 | func TestResolveRecipeFilePath(t *testing.T) { 23 | testCases := []struct { 24 | id string 25 | recipe types.RecipeName 26 | filepath string 27 | err error 28 | }{{ 29 | id: "file doesn't exist", 30 | recipe: "", 31 | filepath: "", 32 | err: os.ErrNotExist, 33 | }, { 34 | id: "apache dot grlx", 35 | recipe: "apache.grlx", 36 | filepath: "", 37 | err: os.ErrNotExist, 38 | }, { 39 | id: "apache dot apache dot grlx", 40 | recipe: "apache.apache.grlx", 41 | filepath: filepath.Join(getBasePath(), "apache/apache.grlx"), 42 | err: nil, 43 | }, { 44 | id: "apache slash path", 45 | recipe: "apache/apache", 46 | filepath: filepath.Join(getBasePath(), "apache/apache.grlx"), 47 | err: nil, 48 | }, { 49 | id: "apache dot path", 50 | recipe: "apache.apache", 51 | filepath: filepath.Join(getBasePath(), "apache/apache.grlx"), 52 | err: nil, 53 | }, { 54 | id: "dev", 55 | recipe: "dev", 56 | filepath: filepath.Join(getBasePath(), "dev.grlx"), 57 | err: nil, 58 | }, { 59 | id: "apache init", 60 | recipe: "apache", 61 | filepath: filepath.Join(getBasePath(), "apache/init.grlx"), 62 | err: nil, 63 | }} 64 | for _, tc := range testCases { 65 | t.Run(tc.id, func(t *testing.T) { 66 | filepath, err := ResolveRecipeFilePath(getBasePath(), tc.recipe) 67 | if filepath != tc.filepath { 68 | t.Errorf("expected %s but got %s", tc.filepath, filepath) 69 | } 70 | if !errors.Is(err, tc.err) { 71 | t.Errorf("expected error %v but got %v", tc.err, err) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | // func TestParseRecipeFile(t *testing.T) { 78 | // testCases := []struct { 79 | // id string 80 | // recipe types.RecipeName 81 | // recipeSteps []types.RecipeCooker 82 | // }{} 83 | // 84 | // for _, tc := range testCases { 85 | // t.Run(tc.id, func(t *testing.T) { 86 | // steps := ParseRecipeFile(tc.recipe) 87 | // _ = steps 88 | // }) 89 | // } 90 | // } 91 | -------------------------------------------------------------------------------- /api/handlers/ingredients/test/ping.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | 10 | //. "github.com/gogrlx/grlx/v2/config" 11 | "github.com/gogrlx/grlx/v2/ingredients/test" 12 | "github.com/gogrlx/grlx/v2/pki" 13 | . "github.com/gogrlx/grlx/v2/types" 14 | log "github.com/taigrr/log-socket/log" 15 | ) 16 | 17 | // TODO: add callback event for when new key is PUT to the server 18 | func HTestPing(w http.ResponseWriter, r *http.Request) { 19 | var targetAction TargetedAction 20 | // grab the body of the req 21 | err := json.NewDecoder(r.Body).Decode(&targetAction) 22 | if err != nil { 23 | log.Trace("An invalid ping request was made.") 24 | http.Error(w, err.Error(), http.StatusBadRequest) 25 | return 26 | } 27 | jw, _ := json.Marshal(targetAction.Action) 28 | var ping PingPong 29 | json.NewDecoder(bytes.NewBuffer(jw)).Decode(&ping) 30 | 31 | // verify our sprout id is valid 32 | for _, target := range targetAction.Target { 33 | if !pki.IsValidSproutID(target.SproutID) || strings.Contains(target.SproutID, "_") { 34 | log.Trace("An invalid Sprout ID was submitted. Ignoring.") 35 | w.WriteHeader(http.StatusBadRequest) 36 | return 37 | } 38 | registered, _ := pki.NKeyExists(target.SproutID, "") 39 | if !registered { 40 | var results TargetedResults 41 | results.Results = nil 42 | log.Trace("An unknown Sprout was pinged. Ignoring.") 43 | jw, _ := json.Marshal(results) 44 | w.WriteHeader(http.StatusNotFound) 45 | w.Write(jw) 46 | return 47 | } 48 | } 49 | 50 | // check if the id exists in any of the folders 51 | // if it does, append a counter to the end, and check again 52 | // if we hit 100 sprouts with the same id, kick back a StatusBadRequest 53 | var results TargetedResults 54 | results.Results = make(map[string]interface{}) 55 | var wg sync.WaitGroup 56 | var m sync.Mutex 57 | for _, target := range targetAction.Target { 58 | wg.Add(1) 59 | 60 | go func(target KeyManager) { 61 | defer wg.Done() 62 | pong, err := test.FPing(target, ping) 63 | if err != nil { 64 | log.Tracef("Error pinging the Sprout: %v", err) 65 | } 66 | m.Lock() 67 | results.Results[target.SproutID] = pong 68 | m.Unlock() 69 | }(target) 70 | } 71 | wg.Wait() 72 | jr, err := json.Marshal(results) 73 | if err != nil { 74 | log.Error(err) 75 | } 76 | w.WriteHeader(http.StatusOK) 77 | w.Write(jr) 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /ingredients/user/userPresent.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "os/user" 9 | "strings" 10 | 11 | "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | func (u User) present(ctx context.Context, test bool) (types.Result, error) { 15 | var result types.Result 16 | 17 | userName, ok := u.params["name"].(string) 18 | if !ok { 19 | result.Failed = true 20 | result.Succeeded = false 21 | return result, errors.New("invalid user; name must be a string") 22 | } 23 | 24 | uid := "" 25 | gid := "" 26 | shell := "" 27 | groups := []string{} 28 | home := "" 29 | if uidInter, ok := u.params["uid"]; ok { 30 | uid, ok = uidInter.(string) 31 | } 32 | if gidInter, ok := u.params["gid"]; ok { 33 | gid, ok = gidInter.(string) 34 | } 35 | if shellInter, ok := u.params["shell"]; ok { 36 | shell, ok = shellInter.(string) 37 | } 38 | if groupsInter, ok := u.params["groups"]; ok { 39 | groups, ok = groupsInter.([]string) 40 | } 41 | if homeInter, ok := u.params["home"]; ok { 42 | home, ok = homeInter.(string) 43 | } 44 | userCmd := "usermod" 45 | user, err := user.Lookup(userName) 46 | if err != nil { 47 | userCmd = "useradd" 48 | } 49 | args := []string{userName} 50 | if uid != "" && user == nil || uid != user.Uid { 51 | args = append(args, "-u"+uid) 52 | } 53 | if gid != "" && user == nil || gid != user.Gid { 54 | args = append(args, "-g"+gid) 55 | } 56 | if shell != "" { 57 | args = append(args, "-s"+shell) 58 | } 59 | if home != "" && shell != user.HomeDir { 60 | args = append(args, "-d"+home) 61 | } 62 | if len(groups) > 0 { 63 | args = append(args, "-G"+strings.Join(groups, ",")) 64 | } 65 | cmd := exec.CommandContext(ctx, userCmd, args...) 66 | if test { 67 | result.Notes = append(result.Notes, 68 | types.SimpleNote(fmt.Sprintf("would have updated user with command: %s", cmd.String()))) 69 | result.Succeeded = true 70 | result.Failed = false 71 | result.Changed = true 72 | return result, nil 73 | } 74 | err = cmd.Run() 75 | if err != nil { 76 | result.Failed = true 77 | result.Succeeded = false 78 | result.Notes = append(result.Notes, types.SimpleNote(fmt.Sprintf("failed to update user: %s", err.Error()))) 79 | return result, err 80 | } 81 | result.Failed = false 82 | result.Succeeded = true 83 | result.Changed = true 84 | result.Notes = append(result.Notes, types.SimpleNote(fmt.Sprintf("updated user with command: %s", cmd.String()))) 85 | return result, nil 86 | } 87 | -------------------------------------------------------------------------------- /cmd/sprout/nats.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "runtime" 7 | 8 | log "github.com/taigrr/log-socket/log" 9 | 10 | "github.com/gogrlx/grlx/v2/cook" 11 | "github.com/gogrlx/grlx/v2/ingredients/cmd" 12 | "github.com/gogrlx/grlx/v2/ingredients/test" 13 | "github.com/gogrlx/grlx/v2/pki" 14 | "github.com/gogrlx/grlx/v2/types" 15 | 16 | nats "github.com/nats-io/nats.go" 17 | ) 18 | 19 | func init() { 20 | createConfigRoot() 21 | pki.SetupPKISprout() 22 | } 23 | 24 | func natsInit(nc *nats.Conn) error { 25 | log.Debugf("Announcing on Farmer...") 26 | startup := types.Startup{} 27 | startup.Version.Arch = runtime.GOARCH 28 | startup.Version.Compiler = runtime.Version() 29 | startup.Version.GitCommit = GitCommit 30 | startup.Version.Tag = Tag 31 | startup.SproutID = sproutID 32 | startupEvent := "grlx.sprouts.announce." + sproutID 33 | b, _ := json.Marshal(startup) 34 | err := nc.Publish(startupEvent, b) 35 | if err != nil { 36 | return err 37 | } 38 | if err = nc.LastError(); err != nil { 39 | log.Fatal(err) 40 | } else { 41 | log.Tracef("Successfully published startup message on `%s`.", startupEvent) 42 | } 43 | 44 | _, err = nc.Subscribe("grlx.sprouts."+sproutID+".cmd.run", func(m *nats.Msg) { 45 | var cmdRun types.CmdRun 46 | json.NewDecoder(bytes.NewBuffer(m.Data)).Decode(&cmdRun) 47 | log.Trace(cmdRun) 48 | results, err := cmd.SRun(cmdRun) 49 | if err != nil { 50 | log.Error(err) 51 | } 52 | resultsB, err := json.Marshal(results) 53 | if err != nil { 54 | log.Error(err) 55 | } 56 | m.Respond(resultsB) 57 | }) 58 | if err != nil { 59 | return err 60 | } 61 | _, err = nc.Subscribe("grlx.sprouts."+sproutID+".test.ping", func(m *nats.Msg) { 62 | var ping types.PingPong 63 | json.NewDecoder(bytes.NewBuffer(m.Data)).Decode(&ping) 64 | log.Trace(ping) 65 | pong, _ := test.SPing(ping) 66 | pongB, _ := json.Marshal(pong) 67 | m.Respond(pongB) 68 | }) 69 | if err != nil { 70 | return err 71 | } 72 | _, err = nc.Subscribe("grlx.sprouts."+sproutID+".cook", func(m *nats.Msg) { 73 | var rEnvelope types.RecipeEnvelope 74 | json.NewDecoder(bytes.NewBuffer(m.Data)).Decode(&rEnvelope) 75 | log.Trace(rEnvelope) 76 | ackB, _ := json.Marshal(types.Ack{Acknowledged: true, JobID: rEnvelope.JobID}) 77 | m.Respond(ackB) 78 | go func() { 79 | err = cook.CookRecipeEnvelope(rEnvelope) 80 | if err != nil { 81 | log.Error(err) 82 | } 83 | }() 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /auth/sign.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | "time" 9 | 10 | "github.com/nats-io/nkeys" 11 | ) 12 | 13 | type UserAuth struct { 14 | Expires string `json:"expires"` 15 | Pubkey string `json:"pubkey"` 16 | Sig string `json:"sig"` 17 | } 18 | 19 | var ErrExpired = errors.New("auth token expired") 20 | 21 | // Sign adds a signature digest to the UserAuth struct using the provided 22 | // KeyPair. The signature digest is base64 encoded. 23 | func (u UserAuth) Sign(kp nkeys.KeyPair) (UserAuth, error) { 24 | b, err := kp.Sign([]byte(u.Expires)) 25 | if err != nil { 26 | return u, err 27 | } 28 | u.Sig = base64.StdEncoding.EncodeToString(b) 29 | return u, nil 30 | } 31 | 32 | // IsValid checks if the token is valid. It returns the public key 33 | // if valid, or an error if not. 34 | // Note this checks the signature using the public key in the token, 35 | // which is not necessarily a public key that is trusted by the server. 36 | func (u UserAuth) IsValid() (string, error) { 37 | exp, err := time.Parse(time.RFC3339, u.Expires) 38 | if err != nil { 39 | return "", err 40 | } 41 | if exp.Before(time.Now()) { 42 | return "", ErrExpired 43 | } 44 | kp, err := nkeys.FromPublicKey(u.Pubkey) 45 | if err != nil { 46 | return "", err 47 | } 48 | sig, err := base64.StdEncoding.DecodeString(u.Sig) 49 | if err != nil { 50 | return "", err 51 | } 52 | return u.Pubkey, kp.Verify([]byte(u.Expires), sig) 53 | } 54 | 55 | // decodeToken decodes a base64 encoded token and returns the UserAuth 56 | // struct. The token is not validated. 57 | func decodeToken(token string) (UserAuth, error) { 58 | var ua UserAuth 59 | b, err := base64.StdEncoding.DecodeString(token) 60 | if err != nil { 61 | return ua, err 62 | } 63 | err = json.Unmarshal(b, &ua) 64 | return ua, err 65 | } 66 | 67 | // createSignedToken creates a signed token that can be used to authenticate 68 | // with the server. The token is valid for 5 minutes, and is base64 encoded. 69 | func createSignedToken(kp nkeys.KeyPair) (string, error) { 70 | pk, err := kp.PublicKey() 71 | if err != nil { 72 | log.Fatal("error getting public key", err) 73 | } 74 | 75 | ua := UserAuth{ 76 | Expires: time.Now().Add(time.Duration(time.Minute) * 5).Format(time.RFC3339), 77 | Pubkey: pk, 78 | } 79 | ua, err = ua.Sign(kp) 80 | if err != nil { 81 | log.Fatal("error signing", err) 82 | } 83 | b, err := json.Marshal(ua) 84 | if err != nil { 85 | return "", err 86 | } 87 | return base64.StdEncoding.EncodeToString(b), nil 88 | } 89 | -------------------------------------------------------------------------------- /ingredients/file/fileAppend.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/gogrlx/grlx/v2/types" 12 | ) 13 | 14 | func (f File) append(ctx context.Context, test bool) (types.Result, error) { 15 | // TODO 16 | // "name": "string", "text": "[]string", "makedirs": "bool", 17 | // "source": "string", "source_hash": "string", 18 | // "template": "bool", "sources": "[]string", 19 | // "source_hashes": "[]string", 20 | var notes []fmt.Stringer 21 | name, ok := f.params["name"].(string) 22 | if !ok { 23 | return types.Result{ 24 | Succeeded: false, Failed: true, Notes: notes, 25 | }, types.ErrMissingName 26 | } 27 | name = filepath.Clean(name) 28 | if name == "/" { 29 | return types.Result{ 30 | Succeeded: false, Failed: true, Notes: notes, 31 | }, types.ErrModifyRoot 32 | } 33 | res, missing, err := f.contains(ctx, test) 34 | notes = append(notes, res.Notes...) 35 | if err == nil { 36 | return types.Result{ 37 | Succeeded: res.Succeeded, Failed: res.Failed, 38 | Changed: res.Changed, Notes: notes, 39 | }, err 40 | } 41 | if os.IsNotExist(err) { 42 | f, err := os.Create(name) 43 | if err != nil { 44 | return types.Result{ 45 | Succeeded: false, Failed: true, Notes: notes, 46 | }, err 47 | } 48 | defer f.Close() 49 | _, writeErr := missing.WriteTo(f) 50 | if writeErr != nil { 51 | return types.Result{ 52 | Succeeded: false, Failed: true, Notes: notes, 53 | }, err 54 | } 55 | notes = append(notes, types.Snprintf("appended %v", name)) 56 | return types.Result{ 57 | Succeeded: true, Failed: false, 58 | Changed: true, Notes: notes, 59 | }, nil 60 | } 61 | if errors.Is(err, types.ErrMissingContent) { 62 | f, err := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0o644) 63 | // TODO: Bug consider muxing errors to make this more descriptive of the issue that occurred 64 | if err != nil { 65 | return types.Result{ 66 | Succeeded: false, Failed: true, Notes: notes, 67 | }, err 68 | } 69 | defer f.Close() 70 | scanner := bufio.NewScanner(&missing) 71 | line := "" 72 | for scanner.Scan() { 73 | line = scanner.Text() 74 | _, err := f.WriteString(line) 75 | if err != nil { 76 | return types.Result{ 77 | Succeeded: false, Failed: true, Notes: notes, 78 | }, err 79 | } 80 | } 81 | notes = append(notes, types.Snprintf("appended %v", name)) 82 | return types.Result{ 83 | Succeeded: true, Failed: false, 84 | Changed: true, Notes: notes, 85 | }, nil 86 | } 87 | return types.Result{ 88 | Succeeded: false, Failed: true, 89 | Changed: false, Notes: notes, 90 | }, err 91 | } 92 | -------------------------------------------------------------------------------- /props/props.go: -------------------------------------------------------------------------------- 1 | package props 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type expProp struct { 11 | Value interface{} 12 | Expiry time.Time 13 | } 14 | 15 | var ( 16 | propCache = make(map[string]map[string]expProp) 17 | propCacheLock = sync.RWMutex{} 18 | ) 19 | 20 | func init() { 21 | propCache = make(map[string]map[string]expProp) 22 | } 23 | 24 | func GetStringPropFunc(sproutID string) func(string) string { 25 | return func(name string) string { 26 | return getStringProp(sproutID, name) 27 | } 28 | } 29 | 30 | // TODO: implement GetProp 31 | func getStringProp(sproutID, name string) string { 32 | // return "props" 33 | if propCache[sproutID] == nil { 34 | // get from sprout 35 | return "" 36 | } 37 | if propCache[sproutID][name] == (expProp{}) { 38 | // get from sprout 39 | return "" 40 | } 41 | if propCache[sproutID][name].Expiry.Before(time.Now()) { 42 | // get from sprout 43 | delete(propCache[sproutID], name) 44 | return "" 45 | } 46 | return fmt.Sprintf("%v", propCache[sproutID][name]) 47 | } 48 | 49 | func SetPropFunc(sproutID string) func(string, string) error { 50 | return func(name, value string) error { 51 | return setProp(sproutID, name, value) 52 | } 53 | } 54 | 55 | // TODO: implement SetProp 56 | func setProp(sproutID, name, value string) error { 57 | return nil 58 | } 59 | 60 | func GetDeletePropFunc(sproutID string) func(string) error { 61 | return func(name string) error { 62 | return deleteProp(sproutID, name) 63 | } 64 | } 65 | 66 | // TODO: implement DeleteProp 67 | func deleteProp(sproutID, name string) error { 68 | return nil 69 | } 70 | 71 | func GetPropsFunc(sproutID string) func() map[string]interface{} { 72 | return func() map[string]interface{} { 73 | return getProps(sproutID) 74 | } 75 | } 76 | 77 | // TODO: implement getProps 78 | func getProps(sproutID string) map[string]interface{} { 79 | if propCache[sproutID] == nil { 80 | // get from sprout 81 | return nil 82 | } 83 | props := make(map[string]interface{}) 84 | for k, v := range propCache[sproutID] { 85 | if v.Expiry.Before(time.Now()) { 86 | // get from sprout 87 | delete(propCache[sproutID], k) 88 | continue 89 | } 90 | props[k] = v.Value 91 | } 92 | return props 93 | } 94 | 95 | func GetHostnameFunc(sproutID string) func() string { 96 | return func() string { 97 | return hostname(sproutID) 98 | } 99 | } 100 | 101 | func hostname(sproutID string) string { 102 | hostname, err := os.Hostname() 103 | if err != nil { 104 | return "localhost" 105 | } 106 | return hostname 107 | } 108 | -------------------------------------------------------------------------------- /ingredients/file/http/provider.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | httpc "net/http" 8 | "os" 9 | 10 | // "github.com/gogrlx/grlx/v2/ingredients/file" 11 | "github.com/gogrlx/grlx/v2/ingredients/file/hashers" 12 | "github.com/gogrlx/grlx/v2/types" 13 | ) 14 | 15 | type HTTPFile struct { 16 | ID string 17 | Source string 18 | Destination string 19 | Hash string 20 | Props map[string]interface{} 21 | } 22 | 23 | func (hf HTTPFile) Download(ctx context.Context) error { 24 | dest, err := os.Create(hf.Destination) 25 | if err != nil { 26 | return err 27 | } 28 | defer dest.Close() 29 | 30 | method := httpc.MethodGet 31 | if hf.Props["method"] != nil { 32 | if m, okM := hf.Props["method"].(string); okM { 33 | method = m 34 | } 35 | } 36 | // TODO add headers, other body settings, etc here 37 | req, err := httpc.NewRequestWithContext(ctx, method, hf.Source, nil) 38 | if err != nil { 39 | return err 40 | } 41 | res, err := httpc.DefaultClient.Do(req) 42 | if err != nil { 43 | return err 44 | } 45 | defer res.Body.Close() 46 | expectedCode := httpc.StatusOK 47 | if hf.Props["expectedCode"] != nil { 48 | if ec, okEC := hf.Props["expectedCode"].(int); okEC { 49 | expectedCode = ec 50 | } 51 | } 52 | if res.StatusCode != expectedCode { 53 | // TODO standardize this error message 54 | return fmt.Errorf("unexpected HTTP status code %d", res.StatusCode) 55 | } 56 | _, err = io.Copy(dest, res.Body) 57 | if err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | 63 | func (hf HTTPFile) Properties() (map[string]interface{}, error) { 64 | return hf.Props, nil 65 | } 66 | 67 | func (hf HTTPFile) Parse(id, source, destination, hash string, properties map[string]interface{}) (types.FileProvider, error) { 68 | if properties == nil { 69 | properties = make(map[string]interface{}) 70 | } 71 | return HTTPFile{ID: id, Source: source, Destination: destination, Hash: hash, Props: properties}, nil 72 | } 73 | 74 | func (hf HTTPFile) Protocols() []string { 75 | return []string{"http", "https"} 76 | } 77 | 78 | func (lf HTTPFile) Verify(ctx context.Context) (bool, error) { 79 | hashType := "" 80 | if lf.Props["hashType"] == nil { 81 | hashType = hashers.GuessHashType(lf.Hash) 82 | } else if ht, ok := lf.Props["hashType"].(string); !ok { 83 | hashType = hashers.GuessHashType(lf.Hash) 84 | } else { 85 | hashType = ht 86 | } 87 | cf := hashers.CacheFile{ 88 | ID: lf.ID, 89 | Destination: lf.Destination, 90 | Hash: lf.Hash, 91 | HashType: hashType, 92 | } 93 | return cf.Verify(ctx) 94 | } 95 | 96 | // func init() { 97 | // file.RegisterProvider(HTTPFile{}) 98 | // } 99 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | > 1.0 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Contact security@grlx.dev with any relevant info including: 12 | 13 | - criticality 14 | - a summary 15 | - any reproduction information 16 | - whether or not you desire attribution 17 | - patches (if you have them) 18 | 19 | Turnaround time should be within a matter of hours, at most 72 for confirmation. 20 | 21 | If your vulnerability is accepted, we will release a new hotfix and notify users. 22 | We will explain the vulnerability to the community after a reasonable amount of 23 | time has elapsed, allowing users the chance to update, and you will be credited at 24 | this time, if you desire. 25 | 26 | ## Release Signatures 27 | 28 | All grlx releases are cryptographically signed with GPG to ensure authenticity and integrity. 29 | 30 | ### GPG Key Information 31 | 32 | - **Key ID**: `33DCE4DD` 33 | - **Fingerprint**: `3F62 7C68 8B72 ACC6 BC4C A9A7 1E0B 7A1D 33DC E4DD` 34 | - **Owner**: grlx signing key 35 | 36 | ### Importing the Public Key 37 | 38 | ```bash 39 | # Method 1: From key servers 40 | gpg --keyserver keyserver.ubuntu.com --recv-keys 33DCE4DD 41 | 42 | # Method 2: From this repository 43 | curl -s https://raw.githubusercontent.com/gogrlx/grlx/master/gpg-public-key.asc | gpg --import 44 | 45 | # Method 3: Manual import 46 | gpg --import gpg-public-key.asc 47 | ``` 48 | 49 | ### Verifying Downloads 50 | 51 | #### GitHub Releases 52 | ```bash 53 | # Download the files 54 | curl -LO https://github.com/gogrlx/grlx/releases/download/v1.0.0/checksums.txt 55 | curl -LO https://github.com/gogrlx/grlx/releases/download/v1.0.0/checksums.txt.sig 56 | 57 | # Verify signature 58 | gpg --verify checksums.txt.sig checksums.txt 59 | 60 | # Verify binary checksum 61 | sha256sum -c checksums.txt --ignore-missing 62 | ``` 63 | 64 | #### S3 Artifacts 65 | ```bash 66 | # Download from S3 67 | curl -LO https://artifacts.grlx.dev/linux/amd64/latest/grlx 68 | curl -LO https://artifacts.grlx.dev/linux/amd64/latest/checksums.txt 69 | curl -LO https://artifacts.grlx.dev/linux/amd64/latest/checksums.txt.sig 70 | 71 | # Verify signature and checksum 72 | gpg --verify checksums.txt.sig checksums.txt 73 | sha256sum -c checksums.txt --ignore-missing 74 | ``` 75 | 76 | ### Trust Verification 77 | 78 | After importing the key, verify the fingerprint matches: 79 | 80 | ```bash 81 | gpg --fingerprint 33DCE4DD 82 | ``` 83 | 84 | Expected output: 85 | ``` 86 | pub ed25519 2025-06-08 [SC] 87 | 3F62 7C68 8B72 ACC6 BC4C A9A7 1E0B 7A1D 33DC E4DD 88 | uid grlx signing key 89 | sub cv25519 2025-06-08 [E] 90 | ``` 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gogrlx/grlx/v2 2 | 3 | go 1.24.4 4 | 5 | replace github.com/mattn/go-localereader v0.0.1 => github.com/taigrr/go-localereader v0.0.2 6 | 7 | require ( 8 | github.com/charmbracelet/bubbles v0.21.0 9 | github.com/charmbracelet/bubbletea v1.3.5 10 | github.com/charmbracelet/fang v0.1.0 11 | github.com/charmbracelet/lipgloss v1.1.0 12 | github.com/djherbis/atime v1.1.0 13 | github.com/fatih/color v1.18.0 14 | github.com/google/uuid v1.6.0 15 | github.com/gorilla/mux v1.8.1 16 | github.com/nats-io/nats-server/v2 v2.11.4 17 | github.com/nats-io/nats.go v1.43.0 18 | github.com/nats-io/nkeys v0.4.11 19 | github.com/spf13/cobra v1.9.1 20 | github.com/taigrr/jety v0.0.12 21 | github.com/taigrr/log-socket v1.0.3 22 | github.com/taigrr/systemctl v1.0.10 23 | gopkg.in/yaml.v3 v3.0.1 24 | ) 25 | 26 | require ( 27 | github.com/BurntSushi/toml v1.5.0 // indirect 28 | github.com/atotto/clipboard v0.1.4 // indirect 29 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 30 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 31 | github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 // indirect 32 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 33 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 34 | github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect 35 | github.com/charmbracelet/x/term v0.2.1 // indirect 36 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 37 | github.com/google/go-tpm v0.9.5 // indirect 38 | github.com/gorilla/websocket v1.5.3 // indirect 39 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 40 | github.com/klauspost/compress v1.18.0 // indirect 41 | github.com/kr/pretty v0.3.1 // indirect 42 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 43 | github.com/mattn/go-colorable v0.1.14 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/mattn/go-localereader v0.0.1 // indirect 46 | github.com/mattn/go-runewidth v0.0.16 // indirect 47 | github.com/minio/highwayhash v1.0.3 // indirect 48 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 49 | github.com/muesli/cancelreader v0.2.2 // indirect 50 | github.com/muesli/mango v0.1.0 // indirect 51 | github.com/muesli/mango-cobra v1.2.0 // indirect 52 | github.com/muesli/mango-pflag v0.1.0 // indirect 53 | github.com/muesli/roff v0.1.0 // indirect 54 | github.com/muesli/termenv v0.16.0 // indirect 55 | github.com/nats-io/jwt/v2 v2.7.4 // indirect 56 | github.com/nats-io/nuid v1.0.1 // indirect 57 | github.com/rivo/uniseg v0.4.7 // indirect 58 | github.com/spf13/pflag v1.0.6 // indirect 59 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 60 | golang.org/x/crypto v0.39.0 // indirect 61 | golang.org/x/sync v0.15.0 // indirect 62 | golang.org/x/sys v0.33.0 // indirect 63 | golang.org/x/text v0.26.0 // indirect 64 | golang.org/x/time v0.12.0 // indirect 65 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /ingredients/file/fileSymlink.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | // symlink creates a symlink at the given path 13 | // 14 | // the expected outcome is that name is a symlink pointing to target 15 | // if force is true, then name will be replaced if it already exists 16 | // if backupname is set, then name will be backed up to backupname if it already exists 17 | // and force is true, and name is not a symlink 18 | // if makedirs is true, then any missing directories in the path will be created 19 | // if user is set, then the symlink will be owned by that user 20 | // if group is set, then the symlink will be owned by that group 21 | // if mode is set, then the symlink will be set to that mode 22 | // if test is true, then the symlink will not be created, but the result will indicate 23 | // what would have happened 24 | func (f File) symlink(ctx context.Context, test bool) (types.Result, error) { 25 | // parameters to implement: 26 | // "name": "string", "target": "string", "force": "bool", "backupname": "string", 27 | // "makedirs": "bool", "user": "string", "group": "string", "mode": "string", 28 | var notes []fmt.Stringer 29 | name, ok := f.params["name"].(string) 30 | if !ok { 31 | return types.Result{ 32 | Succeeded: false, Failed: true, 33 | }, types.ErrMissingName 34 | } 35 | name = filepath.Clean(name) 36 | if name == "" { 37 | return types.Result{ 38 | Succeeded: false, Failed: true, 39 | }, types.ErrMissingName 40 | } 41 | if name == "/" { 42 | return types.Result{ 43 | Succeeded: false, Failed: true, 44 | }, types.ErrModifyRoot 45 | } 46 | target, ok := f.params["target"].(string) 47 | if !ok { 48 | return types.Result{ 49 | Succeeded: false, Failed: true, 50 | }, types.ErrMissingTarget 51 | } 52 | target = filepath.Clean(target) 53 | if target == "" { 54 | return types.Result{ 55 | Succeeded: false, Failed: true, 56 | }, types.ErrMissingTarget 57 | } 58 | 59 | nameStat, err := os.Stat(name) 60 | if os.IsNotExist(err) { 61 | if test { 62 | notes = append(notes, types.Snprintf("would create symlink %s pointing to %s", name, target)) 63 | return types.Result{ 64 | Succeeded: true, Failed: false, 65 | Changed: true, Notes: notes, 66 | }, nil 67 | } 68 | // check if it's not already a symlink 69 | if nameStat == nil || nameStat.Mode()&os.ModeSymlink == 0 { 70 | // create the symlink 71 | err = os.Symlink(target, name) 72 | if err != nil { 73 | return types.Result{ 74 | Succeeded: false, Failed: true, 75 | }, err 76 | } 77 | notes = append(notes, types.Snprintf("created symlink %s pointing to %s", name, target)) 78 | return types.Result{ 79 | Succeeded: true, Failed: false, 80 | Changed: true, Notes: notes, 81 | }, nil 82 | } 83 | } else if err != nil { 84 | return types.Result{ 85 | Succeeded: false, Failed: true, 86 | }, err 87 | } 88 | 89 | return f.undef() 90 | } 91 | -------------------------------------------------------------------------------- /ingredients/file/fileCached.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/gogrlx/grlx/v2/types" 10 | ) 11 | 12 | func (f File) cached(ctx context.Context, test bool) (types.Result, error) { 13 | var notes []fmt.Stringer 14 | source, ok := f.params["source"].(string) 15 | if !ok || source == "" { 16 | // TODO join with an error type for missing params 17 | return types.Result{ 18 | Succeeded: false, Failed: true, Notes: notes, 19 | }, types.ErrMissingSource 20 | } 21 | skipVerify, _ := f.params["skip_verify"].(bool) 22 | hash, ok := f.params["hash"].(string) 23 | if (!ok || hash == "") && !skipVerify { 24 | return types.Result{ 25 | Succeeded: false, Failed: true, Notes: notes, 26 | }, types.ErrMissingHash 27 | } 28 | cacheDest, err := f.dest() 29 | if err != nil { 30 | return types.Result{ 31 | Succeeded: false, Failed: true, 32 | }, err 33 | } 34 | fp, err := NewFileProvider(f.id, source, cacheDest, hash, f.params) 35 | if err != nil { 36 | return types.Result{ 37 | Succeeded: false, Failed: true, 38 | }, err 39 | } 40 | if skipVerify { 41 | _, statErr := os.Stat(cacheDest) 42 | if statErr == nil { 43 | notes = append(notes, types.Snprintf("%s already exists and skipVerify is true", cacheDest)) 44 | return types.Result{ 45 | Succeeded: true, Failed: false, 46 | Changed: false, Notes: notes, 47 | }, nil 48 | } else { 49 | if test { 50 | notes = append(notes, types.Snprintf("%s would be cached", cacheDest)) 51 | return types.Result{ 52 | Succeeded: true, Failed: false, 53 | Changed: true, Notes: notes, 54 | }, nil 55 | } 56 | err = fp.Download(ctx) 57 | if err != nil { 58 | return types.Result{ 59 | Succeeded: false, Failed: true, 60 | }, err 61 | } 62 | notes = append(notes, types.Snprintf("%s has been cached", cacheDest)) 63 | return types.Result{ 64 | Succeeded: true, Failed: false, 65 | Changed: true, Notes: notes, 66 | }, nil 67 | 68 | } 69 | } 70 | valid, errVal := fp.Verify(ctx) 71 | if errVal != nil && !errors.Is(errVal, types.ErrFileNotFound) { 72 | return types.Result{ 73 | Succeeded: false, Failed: true, 74 | }, errVal 75 | } 76 | if !valid { 77 | if test { 78 | notes = append(notes, types.Snprintf("%s would be cached", cacheDest)) 79 | return types.Result{ 80 | Succeeded: true, Failed: false, 81 | Changed: true, Notes: notes, 82 | }, nil 83 | } 84 | err = fp.Download(ctx) 85 | if err != nil { 86 | return types.Result{ 87 | Succeeded: false, Failed: true, 88 | }, err 89 | } 90 | notes = append(notes, types.Snprintf("%s has been cached", cacheDest)) 91 | return types.Result{ 92 | Succeeded: true, Failed: false, 93 | Changed: true, Notes: notes, 94 | }, nil 95 | 96 | } 97 | return types.Result{ 98 | Succeeded: true, Failed: false, 99 | Changed: false, Notes: notes, 100 | }, nil 101 | } 102 | -------------------------------------------------------------------------------- /jobs/jobs.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gogrlx/grlx/v2/types" 11 | ) 12 | 13 | var ( 14 | ingTex sync.Mutex 15 | ingMap map[types.Ingredient]map[string]types.RecipeCooker 16 | ) 17 | 18 | func init() { 19 | ingMap = make(map[types.Ingredient]map[string]types.RecipeCooker) 20 | } 21 | 22 | type MethodProps struct { 23 | Key string 24 | Type string 25 | IsReq bool 26 | } 27 | 28 | type JobRecord struct { 29 | JID string 30 | SproutID string 31 | Timestamp time.Time 32 | Executor types.Executor 33 | Completion types.CompletionStatus 34 | } 35 | type RecordKeeper interface{} 36 | 37 | type MethodPropsSet []MethodProps 38 | 39 | func PropMapToPropSet(pmap map[string]string) (MethodPropsSet, error) { 40 | propset := MethodPropsSet{} 41 | for k, v := range pmap { 42 | if v == "" { 43 | return nil, fmt.Errorf("empty value for key %s", k) 44 | } 45 | split := strings.Split(v, ",") 46 | if len(split) > 2 { 47 | return nil, fmt.Errorf("invalid value for key %s", k) 48 | } 49 | isReq := false 50 | if len(split) == 2 { 51 | if split[1] == "req" { 52 | isReq = true 53 | } else if split[1] != "opt" { 54 | return nil, fmt.Errorf("invalid value for key %s", k) 55 | } 56 | } 57 | switch split[0] { 58 | case "string": 59 | fallthrough 60 | case "[]string": 61 | fallthrough 62 | case "bool": 63 | propset = append(propset, MethodProps{Key: k, Type: split[0], IsReq: isReq}) 64 | default: 65 | return nil, fmt.Errorf("invalid Type value for key %s", k) 66 | } 67 | 68 | } 69 | return propset, nil 70 | } 71 | 72 | func (m MethodPropsSet) ToMap() map[string]string { 73 | ret := make(map[string]string) 74 | for _, v := range m { 75 | ret[v.Key] = v.Type 76 | if v.IsReq { 77 | ret[v.Key] = ret[v.Key] + ",req" 78 | } else { 79 | ret[v.Key] = ret[v.Key] + ",opt" 80 | } 81 | } 82 | return ret 83 | } 84 | 85 | func RegisterAllMethods(step types.RecipeCooker) { 86 | ingTex.Lock() 87 | defer ingTex.Unlock() 88 | name, methods := step.Methods() 89 | _, ok := ingMap[types.Ingredient(name)] 90 | if !ok { 91 | ingMap[types.Ingredient(name)] = make(map[string]types.RecipeCooker) 92 | } 93 | for _, method := range methods { 94 | ingMap[types.Ingredient(name)][method] = step 95 | } 96 | } 97 | 98 | var ( 99 | ErrUnknownIngredient = errors.New("unknown ingredient") 100 | ErrUnknownMethod = errors.New("unknown method") 101 | ) 102 | 103 | func NewRecipeCooker(id types.StepID, ingredient types.Ingredient, method string, params map[string]interface{}) (types.RecipeCooker, error) { 104 | fmt.Printf("cooking %s %s %s\n", id, ingredient, method) 105 | ingTex.Lock() 106 | defer ingTex.Unlock() 107 | fmt.Printf("%v\n", ingMap) 108 | if r, ok := ingMap[ingredient]; ok { 109 | if ing, ok := r[method]; ok { 110 | return ing.Parse(string(id), method, params) 111 | } 112 | return nil, ErrUnknownMethod 113 | } 114 | return nil, ErrUnknownIngredient 115 | } 116 | -------------------------------------------------------------------------------- /cook/sproutcook_test.go: -------------------------------------------------------------------------------- 1 | package cook 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gogrlx/grlx/v2/types" 8 | ) 9 | 10 | func TestRequisitesAreMet(t *testing.T) { 11 | // TODO 12 | 13 | completionmap := map[types.StepID]types.StepCompletion{ 14 | "failed": { 15 | ID: "failed", 16 | CompletionStatus: types.StepFailed, 17 | }, 18 | "succeeded": { 19 | ID: "succeeded", 20 | CompletionStatus: types.StepCompleted, 21 | }, 22 | "inprogress": { 23 | ID: "inprogress", 24 | CompletionStatus: types.StepInProgress, 25 | }, 26 | "notstarted": { 27 | ID: "notstarted", 28 | CompletionStatus: types.StepNotStarted, 29 | }, 30 | } 31 | 32 | testCases := []struct { 33 | id string 34 | requisites types.RequisiteSet 35 | expected bool 36 | err error 37 | }{ 38 | { 39 | id: "no reqs", 40 | requisites: types.RequisiteSet{}, 41 | expected: true, err: nil, 42 | }, 43 | { 44 | id: "one requisite, not met", 45 | requisites: types.RequisiteSet{types.Requisite{ 46 | Condition: types.Require, 47 | StepIDs: []types.StepID{"failed"}, 48 | }}, 49 | expected: false, err: ErrRequisiteNotMet, 50 | }, 51 | { 52 | id: "one requisite, met", 53 | requisites: types.RequisiteSet{types.Requisite{ 54 | Condition: types.Require, 55 | StepIDs: []types.StepID{"succeeded"}, 56 | }}, 57 | expected: true, err: nil, 58 | }, 59 | { 60 | id: "one requisite, in progress", 61 | requisites: types.RequisiteSet{types.Requisite{ 62 | Condition: types.Require, 63 | StepIDs: []types.StepID{"inprogress"}, 64 | }}, 65 | expected: false, 66 | err: nil, 67 | }, 68 | { 69 | id: "two requisites, one not met", 70 | requisites: types.RequisiteSet{ 71 | types.Requisite{ 72 | Condition: types.Require, 73 | StepIDs: []types.StepID{"succeeded", "failed"}, 74 | }, 75 | }, 76 | expected: false, 77 | err: ErrRequisiteNotMet, 78 | }, 79 | { 80 | id: "two requisites, one met, one pending", 81 | requisites: types.RequisiteSet{ 82 | types.Requisite{ 83 | Condition: types.Require, 84 | StepIDs: []types.StepID{"succeeded", "inprogress"}, 85 | }, 86 | }, 87 | expected: false, err: nil, 88 | }, 89 | { 90 | id: "two anyrequisites, one met, one pending", 91 | requisites: types.RequisiteSet{ 92 | types.Requisite{ 93 | Condition: types.RequireAny, 94 | StepIDs: []types.StepID{"succeeded", "inprogress"}, 95 | }, 96 | }, 97 | expected: true, err: nil, 98 | }, 99 | } 100 | for _, tc := range testCases { 101 | t.Run(tc.id, func(t *testing.T) { 102 | met, err := RequisitesAreMet(types.Step{Requisites: tc.requisites}, completionmap) 103 | if !errors.Is(err, tc.err) { 104 | t.Errorf("expected error %v, got %v", tc.err, err) 105 | } 106 | if met != tc.expected { 107 | t.Errorf("expected %v, got %v", tc.expected, met) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ingredients/cmd/interactive.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "math" 10 | "os" 11 | "os/exec" 12 | "os/user" 13 | "runtime" 14 | "strconv" 15 | "sync" 16 | "syscall" 17 | "time" 18 | 19 | nats "github.com/nats-io/nats.go" 20 | "github.com/taigrr/log-socket/log" 21 | 22 | "github.com/gogrlx/grlx/v2/types" 23 | ) 24 | 25 | var nc *nats.Conn 26 | 27 | func RegisterNatsConn(conn *nats.Conn) { 28 | nc = conn 29 | } 30 | 31 | var envMutex sync.Mutex 32 | 33 | func FRun(target types.KeyManager, cmdRun types.CmdRun) (types.CmdRun, error) { 34 | topic := "grlx.sprouts." + target.SproutID + ".cmd.run" 35 | var results types.CmdRun 36 | b, _ := json.Marshal(cmdRun) 37 | msg, err := nc.Request(topic, b, time.Second*15+cmdRun.Timeout) 38 | if err != nil { 39 | return results, err 40 | } 41 | err = json.Unmarshal(msg.Data, &results) 42 | return results, err 43 | } 44 | 45 | func SRun(cmd types.CmdRun) (types.CmdRun, error) { 46 | ctx, cancel := context.WithTimeout(context.Background(), cmd.Timeout) 47 | defer cancel() 48 | envMutex.Lock() 49 | osPath := os.Getenv("PATH") 50 | newPath := "" 51 | if val, ok := cmd.Env["PATH"]; cmd.Path == "" && (!ok || (ok && val == "")) { 52 | _, err := exec.LookPath(cmd.Command) 53 | if err != nil { 54 | envMutex.Unlock() 55 | cmd.Error = err 56 | return cmd, err 57 | } 58 | } else { 59 | if cmd.Path != "" { 60 | newPath += cmd.Path + string(os.PathListSeparator) 61 | } 62 | if ok && val != "" { 63 | newPath += val + string(os.PathListSeparator) 64 | } 65 | } 66 | os.Setenv("PATH", newPath+osPath) 67 | command := exec.CommandContext(ctx, cmd.Command, cmd.Args...) 68 | os.Setenv("PATH", osPath) 69 | env := os.Environ() 70 | envMutex.Unlock() 71 | for key, val := range cmd.Env { 72 | env = append(env, key+"="+val) 73 | } 74 | command.Env = env 75 | 76 | var uid uint32 77 | // TODO fix for windows support 78 | if cmd.RunAs != "" && runtime.GOOS != "windows" { 79 | u, err := user.Lookup(cmd.RunAs) 80 | if err != nil { 81 | return cmd, err 82 | } 83 | uid64, err := strconv.Atoi(u.Uid) 84 | if err != nil { 85 | return cmd, err 86 | } 87 | if uid64 > math.MaxInt32 { 88 | return cmd, fmt.Errorf("UID %d is invalid", uid64) 89 | } 90 | uid = uint32(uid64) 91 | command.SysProcAttr = &syscall.SysProcAttr{} 92 | command.SysProcAttr.Credential = &syscall.Credential{Uid: uid} 93 | } 94 | command.Dir = cmd.CWD 95 | 96 | // TODO replace os.Stdout/err here with writes to websocket to get live returnable data 97 | var stdoutBuf, stderrBuf bytes.Buffer 98 | command.Stdout = io.MultiWriter(&stdoutBuf) //, os.Stdout) 99 | command.Stderr = io.MultiWriter(&stderrBuf) //, os.Stderr) 100 | timer := time.Now() 101 | err := command.Run() 102 | cmd.Duration = time.Since(timer) 103 | if err != nil { 104 | log.Errorf("cmd.Run() failed with %s\n", err) 105 | } 106 | cmd.Stdout = stdoutBuf.String() 107 | cmd.Stderr = stderrBuf.String() 108 | cmd.ErrCode = command.ProcessState.ExitCode() 109 | return cmd, err 110 | } 111 | --------------------------------------------------------------------------------