From c54062723718324e261ae62670fcad47a9ca830c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:09:10 -0700 Subject: [PATCH 1/6] fix: improve SDK client examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - health.go: show all status fields (components, registry, NATS, agents, jobs, consumers, streams, KV buckets, stores) - job.go: rewrite to use actual SDK methods (Job.Get, Job.List) instead of non-existent Job.Create; trigger job via domain op - audit.go: add Export demonstration - network.go: require OSAPI_INTERFACE instead of defaulting to eth0 which doesn't exist on macOS - all: fix variable shadowing (client := client.New → c :=) - metrics.go: remove, SDK MetricsService was already deleted 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/sdk/client/agent.go | 6 +-- examples/sdk/client/audit.go | 19 +++++-- examples/sdk/client/command.go | 6 +-- examples/sdk/client/file.go | 18 +++---- examples/sdk/client/health.go | 98 +++++++++++++++++++++++++++++++--- examples/sdk/client/job.go | 57 +++++++++++++------- examples/sdk/client/metrics.go | 56 ------------------- examples/sdk/client/network.go | 8 +-- examples/sdk/client/node.go | 16 +++--- 9 files changed, 171 insertions(+), 113 deletions(-) delete mode 100644 examples/sdk/client/metrics.go diff --git a/examples/sdk/client/agent.go b/examples/sdk/client/agent.go index 3bec0cc90..a232f8baf 100644 --- a/examples/sdk/client/agent.go +++ b/examples/sdk/client/agent.go @@ -45,11 +45,11 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() // List all active agents. - list, err := client.Agent.List(ctx) + list, err := c.Agent.List(ctx) if err != nil { log.Fatalf("list agents: %v", err) } @@ -68,7 +68,7 @@ func main() { // Get rich facts for the first agent. hostname := list.Data.Agents[0].Hostname - resp, err := client.Agent.Get(ctx, hostname) + resp, err := c.Agent.Get(ctx, hostname) if err != nil { log.Fatalf("get agent %s: %v", hostname, err) } diff --git a/examples/sdk/client/audit.go b/examples/sdk/client/audit.go index 726820978..44ef1b604 100644 --- a/examples/sdk/client/audit.go +++ b/examples/sdk/client/audit.go @@ -44,11 +44,11 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() // List recent audit entries. - list, err := client.Audit.List(ctx, 10, 0) + list, err := c.Audit.List(ctx, 10, 0) if err != nil { log.Fatalf("list audit: %v", err) } @@ -67,7 +67,7 @@ func main() { // Get a specific audit entry. id := list.Data.Items[0].ID - entry, err := client.Audit.Get(ctx, id) + entry, err := c.Audit.Get(ctx, id) if err != nil { log.Fatalf("get audit %s: %v", id, err) } @@ -77,4 +77,17 @@ func main() { fmt.Printf(" Path: %s\n", entry.Data.Path) fmt.Printf(" User: %s\n", entry.Data.User) fmt.Printf(" Duration: %dms\n", entry.Data.DurationMs) + + // Export all audit entries. + export, err := c.Audit.Export(ctx) + if err != nil { + log.Fatalf("export audit: %v", err) + } + + fmt.Printf("\nExported: %d entries\n", export.Data.TotalItems) + + for _, e := range export.Data.Items { + fmt.Printf(" %s %s %s code=%d\n", + e.ID, e.Method, e.Path, e.ResponseCode) + } } diff --git a/examples/sdk/client/command.go b/examples/sdk/client/command.go index c6ca8e93d..0abdae342 100644 --- a/examples/sdk/client/command.go +++ b/examples/sdk/client/command.go @@ -44,12 +44,12 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() target := "_any" // Direct exec — runs a binary with arguments. - exec, err := client.Node.Exec(ctx, client.ExecRequest{ + exec, err := c.Node.Exec(ctx, client.ExecRequest{ Target: target, Command: "uptime", }) @@ -64,7 +64,7 @@ func main() { } // Shell — interpreted by /bin/sh, supports pipes and redirection. - shell, err := client.Node.Shell(ctx, client.ShellRequest{ + shell, err := c.Node.Shell(ctx, client.ShellRequest{ Target: target, Command: "uname -a", }) diff --git a/examples/sdk/client/file.go b/examples/sdk/client/file.go index f7a202cfa..26d7b5836 100644 --- a/examples/sdk/client/file.go +++ b/examples/sdk/client/file.go @@ -46,12 +46,12 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() // Upload a raw file to the Object Store. content := []byte("listen_address = 0.0.0.0:8080\nworkers = 4\n") - upload, err := client.File.Upload( + upload, err := c.File.Upload( ctx, "app.conf", "raw", @@ -65,7 +65,7 @@ func main() { upload.Data.Name, upload.Data.SHA256, upload.Data.Size, upload.Data.Changed) // Check if the file has changed without uploading. - chk, err := client.File.Changed(ctx, "app.conf", bytes.NewReader(content)) + chk, err := c.File.Changed(ctx, "app.conf", bytes.NewReader(content)) if err != nil { log.Fatalf("changed: %v", err) } @@ -73,7 +73,7 @@ func main() { fmt.Printf("Changed: name=%s changed=%v\n", chk.Data.Name, chk.Data.Changed) // Force upload bypasses both SDK-side and server-side checks. - force, err := client.File.Upload( + force, err := c.File.Upload( ctx, "app.conf", "raw", @@ -88,7 +88,7 @@ func main() { force.Data.Name, force.Data.Changed) // List all stored files. - list, err := client.File.List(ctx) + list, err := c.File.List(ctx) if err != nil { log.Fatalf("list: %v", err) } @@ -99,7 +99,7 @@ func main() { } // Get metadata for a specific file. - meta, err := client.File.Get(ctx, "app.conf") + meta, err := c.File.Get(ctx, "app.conf") if err != nil { log.Fatalf("get: %v", err) } @@ -108,7 +108,7 @@ func main() { meta.Data.Name, meta.Data.SHA256, meta.Data.Size) // Deploy the file to an agent. - deploy, err := client.Node.FileDeploy(ctx, client.FileDeployOpts{ + deploy, err := c.Node.FileDeploy(ctx, client.FileDeployOpts{ ObjectName: "app.conf", Path: "/tmp/app.conf", ContentType: "raw", @@ -123,7 +123,7 @@ func main() { deploy.Data.JobID, deploy.Data.Hostname, deploy.Data.Changed) // Check file status on the agent. - status, err := client.Node.FileStatus(ctx, "_any", "/tmp/app.conf") + status, err := c.Node.FileStatus(ctx, "_any", "/tmp/app.conf") if err != nil { log.Fatalf("status: %v", err) } @@ -132,7 +132,7 @@ func main() { status.Data.Path, status.Data.Status) // Clean up — delete the file from the Object Store. - del, err := client.File.Delete(ctx, "app.conf") + del, err := c.File.Delete(ctx, "app.conf") if err != nil { log.Fatalf("delete: %v", err) } diff --git a/examples/sdk/client/health.go b/examples/sdk/client/health.go index 0b1238678..ee5f581a0 100644 --- a/examples/sdk/client/health.go +++ b/examples/sdk/client/health.go @@ -19,7 +19,8 @@ // DEALINGS IN THE SOFTWARE. // Package main demonstrates the HealthService: liveness, readiness, -// and detailed system status checks. +// and detailed system status including components, NATS info, agents, +// jobs, streams, KV buckets, object stores, and the component registry. // // Run with: OSAPI_TOKEN="" go run health.go package main @@ -44,11 +45,11 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() // Liveness — is the API process running? - live, err := client.Health.Liveness(ctx) + live, err := c.Health.Liveness(ctx) if err != nil { log.Fatalf("liveness: %v", err) } @@ -56,7 +57,7 @@ func main() { fmt.Printf("Liveness: %s\n", live.Data.Status) // Readiness — is the API ready to serve requests? - ready, err := client.Health.Ready(ctx) + ready, err := c.Health.Ready(ctx) if err != nil { log.Fatalf("readiness: %v", err) } @@ -64,12 +65,93 @@ func main() { fmt.Printf("Readiness: %s\n", ready.Data.Status) // Status — detailed system info (requires auth). - status, err := client.Health.Status(ctx) + status, err := c.Health.Status(ctx) if err != nil { log.Fatalf("status: %v", err) } - fmt.Printf("Status: %s\n", status.Data.Status) - fmt.Printf("Version: %s\n", status.Data.Version) - fmt.Printf("Uptime: %s\n", status.Data.Uptime) + s := status.Data + fmt.Printf("\nStatus: %s\n", s.Status) + fmt.Printf("Version: %s\n", s.Version) + fmt.Printf("Uptime: %s\n", s.Uptime) + + // Components (nats, jetstream). + if len(s.Components) > 0 { + fmt.Printf("\nComponents:\n") + for name, comp := range s.Components { + if comp.Error != "" { + fmt.Printf(" %-12s %s (error: %s)\n", name, comp.Status, comp.Error) + } else { + fmt.Printf(" %-12s %s\n", name, comp.Status) + } + } + } + + // NATS connection info. + if s.NATS != nil { + fmt.Printf("\nNATS: url=%s version=%s\n", s.NATS.URL, s.NATS.Version) + } + + // Agent stats. + if s.Agents != nil { + fmt.Printf("\nAgents: %d total, %d ready\n", s.Agents.Total, s.Agents.Ready) + for _, a := range s.Agents.Agents { + fmt.Printf(" %s registered=%s labels=%s\n", + a.Hostname, a.Registered, a.Labels) + } + } + + // Job stats. + if s.Jobs != nil { + fmt.Printf("\nJobs: total=%d completed=%d failed=%d processing=%d unprocessed=%d dlq=%d\n", + s.Jobs.Total, s.Jobs.Completed, s.Jobs.Failed, + s.Jobs.Processing, s.Jobs.Unprocessed, s.Jobs.Dlq) + } + + // Consumer stats. + if s.Consumers != nil { + fmt.Printf("\nConsumers: %d total\n", s.Consumers.Total) + for _, c := range s.Consumers.Consumers { + fmt.Printf(" %-20s pending=%d ack_pending=%d redelivered=%d\n", + c.Name, c.Pending, c.AckPending, c.Redelivered) + } + } + + // Streams. + if len(s.Streams) > 0 { + fmt.Printf("\nStreams:\n") + for _, st := range s.Streams { + fmt.Printf(" %-20s messages=%d bytes=%d consumers=%d\n", + st.Name, st.Messages, st.Bytes, st.Consumers) + } + } + + // KV buckets. + if len(s.KVBuckets) > 0 { + fmt.Printf("\nKV Buckets:\n") + for _, b := range s.KVBuckets { + fmt.Printf(" %-20s keys=%d bytes=%d\n", b.Name, b.Keys, b.Bytes) + } + } + + // Object stores. + if len(s.ObjectStores) > 0 { + fmt.Printf("\nObject Stores:\n") + for _, o := range s.ObjectStores { + fmt.Printf(" %-20s size=%d\n", o.Name, o.Size) + } + } + + // Component registry (agents, API servers, NATS servers). + if len(s.Registry) > 0 { + fmt.Printf("\nRegistry:\n") + for _, e := range s.Registry { + fmt.Printf(" %-6s %-30s status=%-6s age=%-5s cpu=%.1f%% mem=%d", + e.Type, e.Hostname, e.Status, e.Age, e.CPUPercent, e.MemBytes) + if len(e.Conditions) > 0 { + fmt.Printf(" conditions=%v", e.Conditions) + } + fmt.Println() + } + } } diff --git a/examples/sdk/client/job.go b/examples/sdk/client/job.go index 6e2f85a99..4a84df47a 100644 --- a/examples/sdk/client/job.go +++ b/examples/sdk/client/job.go @@ -18,8 +18,11 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package main demonstrates the JobService: creating a job, polling -// for its result, listing jobs, and checking queue statistics. +// Package main demonstrates the JobService: listing jobs, retrieving +// a specific job's details, and viewing timeline events. +// +// Jobs are created implicitly by domain operations (e.g. Node.Hostname, +// Docker.Create). This example shows how to inspect them after the fact. // // Run with: OSAPI_TOKEN="" go run job.go package main @@ -29,7 +32,6 @@ import ( "fmt" "log" "os" - "time" "github.com/retr0h/osapi/pkg/sdk/client" ) @@ -45,40 +47,57 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() - // Create a job. - created, err := client.Job.Create(ctx, map[string]any{ - "type": "node.hostname.get", - }, "_any") + // Trigger a job via a domain operation so we have something to inspect. + hn, err := c.Node.Hostname(ctx, "_any") if err != nil { - log.Fatalf("create job: %v", err) + log.Fatalf("hostname: %v", err) } - fmt.Printf("Created job: %s status=%s\n", - created.Data.JobID, created.Data.Status) - - // Poll until the job completes. - time.Sleep(2 * time.Second) + jobID := hn.Data.JobID + fmt.Printf("Created job via Node.Hostname: %s\n", jobID) - job, err := client.Job.Get(ctx, created.Data.JobID) + // Get the job's full details. + job, err := c.Job.Get(ctx, jobID) if err != nil { log.Fatalf("get job: %v", err) } - fmt.Printf("Job %s: status=%s\n", job.Data.ID, job.Data.Status) + fmt.Printf("\nJob %s:\n", job.Data.ID) + fmt.Printf(" Status: %s\n", job.Data.Status) + fmt.Printf(" Hostname: %s\n", job.Data.Hostname) + fmt.Printf(" Operation: %v\n", job.Data.Operation) + fmt.Printf(" Created: %s\n", job.Data.Created) + + if job.Data.Error != "" { + fmt.Printf(" Error: %s\n", job.Data.Error) + } + + // Timeline events show the job's lifecycle. + if len(job.Data.Timeline) > 0 { + fmt.Printf("\n Timeline:\n") + for _, e := range job.Data.Timeline { + fmt.Printf(" %s %-12s host=%s\n", + e.Timestamp, e.Event, e.Hostname) + } + } - // List recent jobs. - list, err := client.Job.List(ctx, client.ListParams{Limit: 5}) + // List recent jobs with status counts. + list, err := c.Job.List(ctx, client.ListParams{Limit: 10}) if err != nil { log.Fatalf("list jobs: %v", err) } fmt.Printf("\nRecent jobs: %d total\n", list.Data.TotalItems) + if len(list.Data.StatusCounts) > 0 { + fmt.Printf(" Status counts: %v\n", list.Data.StatusCounts) + } + for _, j := range list.Data.Items { - fmt.Printf(" %s status=%s op=%v\n", + fmt.Printf(" %s status=%-10s op=%v\n", j.ID, j.Status, j.Operation) } } diff --git a/examples/sdk/client/metrics.go b/examples/sdk/client/metrics.go deleted file mode 100644 index be442481b..000000000 --- a/examples/sdk/client/metrics.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2026 John Dewey - -// 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: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// 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. - -// Package main demonstrates the MetricsService: fetching raw -// Prometheus metrics text from the /metrics endpoint. -// -// Run with: OSAPI_TOKEN="" go run metrics.go -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/retr0h/osapi/pkg/sdk/client" -) - -func main() { - url := os.Getenv("OSAPI_URL") - if url == "" { - url = "http://localhost:8080" - } - - token := os.Getenv("OSAPI_TOKEN") - if token == "" { - log.Fatal("OSAPI_TOKEN is required") - } - - client := client.New(url, token) - ctx := context.Background() - - text, err := client.Metrics.Get(ctx) - if err != nil { - log.Fatalf("metrics: %v", err) - } - - fmt.Println(text) -} diff --git a/examples/sdk/client/network.go b/examples/sdk/client/network.go index 93f4caf73..f1b792bc3 100644 --- a/examples/sdk/client/network.go +++ b/examples/sdk/client/network.go @@ -46,15 +46,15 @@ func main() { iface := os.Getenv("OSAPI_INTERFACE") if iface == "" { - iface = "eth0" + log.Fatal("OSAPI_INTERFACE is required (e.g. eth0, en0)") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() target := "_any" // Get DNS configuration for an interface. - dns, err := client.Node.GetDNS(ctx, target, iface) + dns, err := c.Node.GetDNS(ctx, target, iface) if err != nil { log.Fatalf("get dns: %v", err) } @@ -66,7 +66,7 @@ func main() { } // Ping a host. - ping, err := client.Node.Ping(ctx, target, "8.8.8.8") + ping, err := c.Node.Ping(ctx, target, "8.8.8.8") if err != nil { log.Fatalf("ping: %v", err) } diff --git a/examples/sdk/client/node.go b/examples/sdk/client/node.go index 3a7b4c8ab..c7d3bcb4d 100644 --- a/examples/sdk/client/node.go +++ b/examples/sdk/client/node.go @@ -44,12 +44,12 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) + c := client.New(url, token) ctx := context.Background() target := "_any" // Status (aggregated node info). - status, err := client.Node.Status(ctx, target) + status, err := c.Node.Status(ctx, target) if err != nil { log.Fatalf("status: %v", err) } @@ -69,7 +69,7 @@ func main() { } // Hostname - hn, err := client.Node.Hostname(ctx, target) + hn, err := c.Node.Hostname(ctx, target) if err != nil { log.Fatalf("hostname: %v", err) } @@ -79,7 +79,7 @@ func main() { } // Disk usage - disk, err := client.Node.Disk(ctx, target) + disk, err := c.Node.Disk(ctx, target) if err != nil { log.Fatalf("disk: %v", err) } @@ -93,7 +93,7 @@ func main() { } // Memory - mem, err := client.Node.Memory(ctx, target) + mem, err := c.Node.Memory(ctx, target) if err != nil { log.Fatalf("memory: %v", err) } @@ -104,7 +104,7 @@ func main() { } // Load averages - load, err := client.Node.Load(ctx, target) + load, err := c.Node.Load(ctx, target) if err != nil { log.Fatalf("load: %v", err) } @@ -118,7 +118,7 @@ func main() { } // OS info - osInfo, err := client.Node.OS(ctx, target) + osInfo, err := c.Node.OS(ctx, target) if err != nil { log.Fatalf("os: %v", err) } @@ -131,7 +131,7 @@ func main() { } // Uptime - up, err := client.Node.Uptime(ctx, target) + up, err := c.Node.Uptime(ctx, target) if err != nil { log.Fatalf("uptime: %v", err) } From 2c26aa334edccecbfcd40ac12b8da9c1f16d75ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:11:19 -0700 Subject: [PATCH 2/6] feat: add ImageRemove to container example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/sdk/client/container.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/sdk/client/container.go b/examples/sdk/client/container.go index 08e471735..5d69c48c3 100644 --- a/examples/sdk/client/container.go +++ b/examples/sdk/client/container.go @@ -146,4 +146,20 @@ func main() { fmt.Printf("Remove (%s): id=%s message=%s\n", r.Hostname, r.ID, r.Message) } + + // Remove the image. + imgRemove, err := c.Docker.ImageRemove( + ctx, + target, + "nginx:alpine", + &client.DockerImageRemoveParams{Force: true}, + ) + if err != nil { + log.Fatalf("image remove: %v", err) + } + + for _, r := range imgRemove.Data.Results { + fmt.Printf("ImageRemove (%s): id=%s message=%s\n", + r.Hostname, r.ID, r.Message) + } } From f82dd9f9491f69e52abff0b209f035b111f20f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:16:07 -0700 Subject: [PATCH 3/6] fix: improve SDK orchestrator examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add missing docker-image-remove.go operation example - Fix hardcoded eth0 in network-dns-get.go and network-dns-update.go — require OSAPI_INTERFACE env var 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../operations/docker-image-remove.go | 99 +++++++++++++++++++ .../operations/network-dns-get.go | 9 +- .../operations/network-dns-update.go | 9 +- 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 examples/sdk/orchestrator/operations/docker-image-remove.go diff --git a/examples/sdk/orchestrator/operations/docker-image-remove.go b/examples/sdk/orchestrator/operations/docker-image-remove.go new file mode 100644 index 000000000..108b7b62d --- /dev/null +++ b/examples/sdk/orchestrator/operations/docker-image-remove.go @@ -0,0 +1,99 @@ +// Copyright (c) 2026 John Dewey + +// 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: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// 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. + +// Package main demonstrates the docker.image.remove operation, which +// removes a container image from the target node. +// +// Run with: OSAPI_TOKEN="" go run docker-image-remove.go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/retr0h/osapi/pkg/sdk/client" + "github.com/retr0h/osapi/pkg/sdk/orchestrator" +) + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + c := client.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf("[%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + }, + } + + plan := orchestrator.NewPlan(c, orchestrator.WithHooks(hooks)) + + plan.TaskFunc( + "remove-image", + func( + ctx context.Context, + cc *client.Client, + ) (*orchestrator.Result, error) { + resp, err := cc.Docker.ImageRemove( + ctx, "_any", "nginx:latest", + &client.DockerImageRemoveParams{Force: true}, + ) + if err != nil { + return nil, err + } + + return orchestrator.CollectionResult(resp.Data, resp.RawJSON(), + func(r client.DockerActionResult) orchestrator.HostResult { + return orchestrator.HostResult{ + Hostname: r.Hostname, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + for _, r := range report.Tasks { + if len(r.Data) > 0 { + b, _ := json.MarshalIndent(r.Data, "", " ") + fmt.Printf("data: %s\n", b) + } + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +} diff --git a/examples/sdk/orchestrator/operations/network-dns-get.go b/examples/sdk/orchestrator/operations/network-dns-get.go index 13499eeb2..e0601d665 100644 --- a/examples/sdk/orchestrator/operations/network-dns-get.go +++ b/examples/sdk/orchestrator/operations/network-dns-get.go @@ -21,7 +21,7 @@ // Package main demonstrates the network.dns.get operation, which // retrieves DNS configuration for a network interface. // -// Run with: OSAPI_TOKEN="" go run network-dns-get.go +// Run with: OSAPI_TOKEN="" OSAPI_INTERFACE=eth0 go run network-dns-get.go package main import ( @@ -46,6 +46,11 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } + iface := os.Getenv("OSAPI_INTERFACE") + if iface == "" { + log.Fatal("OSAPI_INTERFACE is required (e.g. eth0, en0)") + } + c := client.New(url, token) hooks := orchestrator.Hooks{ @@ -63,7 +68,7 @@ func main() { ctx context.Context, cc *client.Client, ) (*orchestrator.Result, error) { - resp, err := cc.Node.GetDNS(ctx, "_any", "eth0") + resp, err := cc.Node.GetDNS(ctx, "_any", iface) if err != nil { return nil, err } diff --git a/examples/sdk/orchestrator/operations/network-dns-update.go b/examples/sdk/orchestrator/operations/network-dns-update.go index 644c3b4ae..bc20bbd16 100644 --- a/examples/sdk/orchestrator/operations/network-dns-update.go +++ b/examples/sdk/orchestrator/operations/network-dns-update.go @@ -21,7 +21,7 @@ // Package main demonstrates the network.dns.update operation, which // updates DNS servers for a network interface. // -// Run with: OSAPI_TOKEN="" go run network-dns-update.go +// Run with: OSAPI_TOKEN="" OSAPI_INTERFACE=eth0 go run network-dns-update.go package main import ( @@ -46,6 +46,11 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } + iface := os.Getenv("OSAPI_INTERFACE") + if iface == "" { + log.Fatal("OSAPI_INTERFACE is required (e.g. eth0, en0)") + } + c := client.New(url, token) hooks := orchestrator.Hooks{ @@ -64,7 +69,7 @@ func main() { cc *client.Client, ) (*orchestrator.Result, error) { resp, err := cc.Node.UpdateDNS( - ctx, "_any", "eth0", + ctx, "_any", iface, []string{"8.8.8.8", "8.8.4.4"}, nil, ) if err != nil { From 8f6b9e9a42c6072eaec62b76fa1affc638e19ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:16:59 -0700 Subject: [PATCH 4/6] docs: add docker-image-remove orchestrator operation page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../operations/docker-image-remove.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md b/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md new file mode 100644 index 000000000..dd9a10e44 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 22 +--- + +# docker.image-remove.execute + +Remove a container image from the host. + +## Usage + +```go +task := plan.TaskFunc("remove-image", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Docker.ImageRemove(ctx, "_any", "nginx:latest", + &client.DockerImageRemoveParams{Force: true}) + if err != nil { + return nil, err + } + + return orchestrator.CollectionResult(resp.Data, resp.RawJSON(), + func(r client.DockerActionResult) orchestrator.HostResult { + return orchestrator.HostResult{ + Hostname: r.Hostname, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------- | ------ | -------- | ----------------------------------------- | +| `image` | string | Yes | Image name or ID (e.g., "nginx:latest") | +| `force` | bool | No | Force removal even if the image is in use | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Idempotent.** Removing an image that does not exist returns success. + +## Permissions + +Requires `docker:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/operations/docker-image-remove.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/operations/docker-image-remove.go) +for a complete working example. From fd26872a13c9360d70564d7ca64825a8311709be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:26:19 -0700 Subject: [PATCH 5/6] fix: whitelist dist/ in .dockerignore for CI builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoReleaser builds binaries into dist/ which the Dockerfile needs to COPY into the image. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index aa47f92de..b6ab649b1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ !cmd/ !internal/ +!dist/ !go.mod !go.sum !main.go From aabfaf21d73a153a345f7744f10bfcad6247490f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Sun, 15 Mar 2026 17:55:19 -0700 Subject: [PATCH 6/6] style: fix table alignment in docker-image-remove doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../sidebar/sdk/orchestrator/operations/docker-image-remove.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md b/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md index dd9a10e44..8bdf74667 100644 --- a/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md +++ b/docs/docs/sidebar/sdk/orchestrator/operations/docker-image-remove.md @@ -35,7 +35,7 @@ task := plan.TaskFunc("remove-image", | Param | Type | Required | Description | | ------- | ------ | -------- | ----------------------------------------- | | `image` | string | Yes | Image name or ID (e.g., "nginx:latest") | -| `force` | bool | No | Force removal even if the image is in use | +| `force` | bool | No | Force removal even if the image is in use | ## Target