From 78d1e44add2e0ea5e73dd97921551199943916fc Mon Sep 17 00:00:00 2001 From: Ondrej Belusky Date: Sat, 21 Feb 2026 12:56:58 +0100 Subject: [PATCH] added nats-exec --- .gitignore | 2 + README.md | 27 ++++++ action.yml | 2 +- cmd/nats-exec/main.go | 146 +++++++++++++++++++++++++++++ main.go => cmd/nats-upload/main.go | 0 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 cmd/nats-exec/main.go rename main.go => cmd/nats-upload/main.go (100%) diff --git a/.gitignore b/.gitignore index 5b90e79..4d47125 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ go.work.sum # env file .env +./bin +bin diff --git a/README.md b/README.md index 64d81b4..635817d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # nats-upload +Tooling for uploading and executing binaries via NATS Object Store. + +## nats-upload + +A GitHub Action to upload binaries to a NATS Object Store for self-update. + +## nats-exec + +A tool to download and execute the latest version of a binary from NATS Object Store. + +### Usage + +```bash +nats-exec [args...] +``` + +Example: +```bash +nats-exec mybinary version +``` + +### Environment Variables + +- `NATS_URL`: NATS server URL (default: `nats://localhost:4222`) +- `NATS_BUCKET`: Object store bucket name (default: `binaries`) +- `NATS_CACHE_DIR`: Local cache directory (default: `/tmp/nats-exec-cache`) + diff --git a/action.yml b/action.yml index c575a40..a9b13bf 100644 --- a/action.yml +++ b/action.yml @@ -39,4 +39,4 @@ inputs: default: 'false' runs: using: 'go' - main: 'main.go' + main: 'cmd/nats-upload/main.go' diff --git a/cmd/nats-exec/main.go b/cmd/nats-exec/main.go new file mode 100644 index 0000000..0eca092 --- /dev/null +++ b/cmd/nats-exec/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "golang.org/x/mod/semver" +) + +func main() { + if len(os.Args) < 2 { + _, _ = fmt.Fprintf(os.Stderr, "Usage: %s [args...]\n", os.Args[0]) + os.Exit(1) + } + + binaryName := os.Args[1] + remainingArgs := os.Args[2:] + + natsURL := getEnv("NATS_URL", "nats://localhost:4222") + bucketName := getEnv("NATS_BUCKET", "binaries") + cacheDir := getEnv("NATS_CACHE_DIR", filepath.Join(os.TempDir(), "nats-exec-cache")) + + ctx := context.Background() + + nc, err := nats.Connect(natsURL) + if err != nil { + log.Fatalf("Failed to connect to NATS: %v", err) + } + defer nc.Close() + + js, err := jetstream.New(nc) + if err != nil { + log.Fatalf("Failed to create JetStream context: %v", err) + } + + store, err := js.ObjectStore(ctx, bucketName) + if err != nil { + log.Fatalf("Failed to get object store %s: %v", bucketName, err) + } + + // Format: binary/arch/version + arch := runtime.GOARCH + + objects, err := store.List(ctx) + if err != nil { + log.Fatalf("Failed to list objects: %v", err) + } + + var latestVersion string + var latestKey string + + for _, info := range objects { + // Key structure: // + + parts := strings.Split(info.Name, "/") + if len(parts) != 3 { + continue + } + if parts[0] != binaryName { + continue + } + if parts[1] != arch { + continue + } + + version := parts[2] + if !strings.HasPrefix(version, "v") { + version = "v" + version + } + + if semver.IsValid(version) { + if latestVersion == "" || semver.Compare(version, latestVersion) > 0 { + latestVersion = version + latestKey = info.Name + } + } + } + + if latestKey == "" { + log.Fatalf("No version of %s found for arch %s", binaryName, arch) + } + + localPath := filepath.Join(cacheDir, latestKey) + + if _, err := os.Stat(localPath); os.IsNotExist(err) { + log.Printf("Downloading %s version %s...", binaryName, latestVersion) + if err := downloadBinary(ctx, store, latestKey, localPath); err != nil { + log.Fatalf("Failed to download binary: %v", err) + } + } else { + log.Printf("Using cached %s version %s", binaryName, latestVersion) + } + + if err := os.Chmod(localPath, 0755); err != nil { + log.Fatalf("Failed to make binary executable: %v", err) + } + + fullPath, err := filepath.Abs(localPath) + if err != nil { + log.Fatalf("Failed to get absolute path: %v", err) + } + + env := os.Environ() + args := append([]string{fullPath}, remainingArgs...) + + err = syscall.Exec(fullPath, args, env) + if err != nil { + log.Fatalf("Failed to exec %s: %v", fullPath, err) + } +} + +func downloadBinary(ctx context.Context, store jetstream.ObjectStore, key, localPath string) error { + if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil { + return err + } + + obj, err := store.Get(ctx, key) + if err != nil { + return err + } + + f, err := os.OpenFile(localPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, obj) + return err +} + +func getEnv(key, defaultValue string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return defaultValue +} diff --git a/main.go b/cmd/nats-upload/main.go similarity index 100% rename from main.go rename to cmd/nats-upload/main.go