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 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..8bdf74667 --- /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. 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/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) + } } 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/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) } diff --git a/examples/sdk/client/metrics.go b/examples/sdk/orchestrator/operations/docker-image-remove.go similarity index 50% rename from examples/sdk/client/metrics.go rename to examples/sdk/orchestrator/operations/docker-image-remove.go index be442481b..108b7b62d 100644 --- a/examples/sdk/client/metrics.go +++ b/examples/sdk/orchestrator/operations/docker-image-remove.go @@ -18,19 +18,21 @@ // 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. +// Package main demonstrates the docker.image.remove operation, which +// removes a container image from the target node. // -// Run with: OSAPI_TOKEN="" go run metrics.go +// 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() { @@ -44,13 +46,54 @@ func main() { log.Fatal("OSAPI_TOKEN is required") } - client := client.New(url, token) - ctx := context.Background() + c := client.New(url, token) - text, err := client.Metrics.Get(ctx) + 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.Fatalf("metrics: %v", err) + 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.Println(text) + 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 {