From 368eaea7c682b5473220e5c937d21f27ffc7b6e6 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: Mon, 6 Apr 2026 11:45:46 -0700 Subject: [PATCH 1/4] feat: add all missing SDK operations and update to latest API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate all existing operations from the old monolithic c.Node/ c.Schedule SDK client to the new per-domain services (c.Hostname, c.DNS, c.Cron, c.FileDeploy, etc.). Add ~75 new operation constructors for 13 domains: - Sysctl: List, Get, Create, Update, Delete - NTP: Get, Create, Update, Delete - Timezone: Get, Update - Service: List, Get, Create, Update, Delete, Start, Stop, Restart, Enable, Disable - Package: List, Get, Install, Remove, Update, ListUpdates - User: List, Get, Create, Update, Delete, ListKeys, AddKey, RemoveKey, ChangePassword - Group: List, Get, Create, Update, Delete - Certificate: List, Create, Update, Delete - Process: List, Get, Signal - Power: Reboot, Shutdown - Log: Query, Sources, QueryUnit - Interface: List, Get, Create, Update, Delete - Route: List, Get, Create, Update, Delete - DNS: Delete (new on existing domain) Add 12 new operation examples demonstrating each new domain. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/features/go.mod | 8 +- examples/features/go.sum | 16 +- examples/operations/certificate.go | 95 + examples/operations/go.mod | 8 +- examples/operations/go.sum | 16 +- examples/operations/interface.go | 90 + examples/operations/log.go | 91 + examples/operations/ntp.go | 87 + examples/operations/package.go | 89 + examples/operations/power.go | 77 + examples/operations/process.go | 75 + examples/operations/route.go | 83 + examples/operations/service.go | 117 ++ examples/operations/sysctl.go | 90 + examples/operations/timezone.go | 73 + examples/operations/user.go | 103 + go.mod | 74 +- go.sum | 142 ++ pkg/orchestrator/ops.go | 2864 +++++++++++++++++++++++++--- 19 files changed, 3860 insertions(+), 338 deletions(-) create mode 100644 examples/operations/certificate.go create mode 100644 examples/operations/interface.go create mode 100644 examples/operations/log.go create mode 100644 examples/operations/ntp.go create mode 100644 examples/operations/package.go create mode 100644 examples/operations/power.go create mode 100644 examples/operations/process.go create mode 100644 examples/operations/route.go create mode 100644 examples/operations/service.go create mode 100644 examples/operations/sysctl.go create mode 100644 examples/operations/timezone.go create mode 100644 examples/operations/user.go diff --git a/examples/features/go.mod b/examples/features/go.mod index ebcce18..cb44c8b 100644 --- a/examples/features/go.mod +++ b/examples/features/go.mod @@ -6,7 +6,7 @@ replace github.com/osapi-io/osapi-orchestrator => ../.. require ( github.com/osapi-io/osapi-orchestrator v0.0.0-00010101000000-000000000000 - github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 + github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 ) require ( @@ -29,9 +29,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.42.0 // indirect ) diff --git a/examples/features/go.sum b/examples/features/go.sum index 899a053..22ab6da 100644 --- a/examples/features/go.sum +++ b/examples/features/go.sum @@ -42,8 +42,8 @@ github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+Y github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 h1:myAfjno/+7oFGdhhi3GPxJnyY8aWAArfsf1BCvRTCdA= -github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150/go.mod h1:rM3VRcxtQ2by9hQpXFHJqMtZZdyaptp9weuICXBde5A= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 h1:xnr/b4Lx+kIYld0+nu/bllZfVFIiPYRow00ykf8tQtE= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616/go.mod h1:K8DSx5/jbgscnu51AJvw9KzWGYdY+L+mmQ6n45dCjwo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -56,12 +56,12 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/operations/certificate.go b/examples/operations/certificate.go new file mode 100644 index 0000000..6992ef3 --- /dev/null +++ b/examples/operations/certificate.go @@ -0,0 +1,95 @@ +// 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 CA certificate management: +// upload a PEM file, create the certificate, update it, then delete. +// +// Cleanup at the start ensures repeatability. +// +// DAG: +// +// upload-file +// └── create-certificate +// └── list-certificate +// └── delete-certificate +// +// Run with: OSAPI_TOKEN="" go run certificate.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const certName = "osapi-example-ca" + + // Placeholder PEM content (not a real certificate). + pem := []byte("-----BEGIN CERTIFICATE-----\nMIIBExample\n-----END CERTIFICATE-----\n") + + // Cleanup any leftover certificate from a previous run. + oc := orchestrator.New(url, token) + oc.CertificateDelete("_any", certName).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Upload PEM → create cert → list certs → delete cert. + o := orchestrator.New(url, token) + + upload := o.FileUpload("osapi-example-ca.pem", "raw", pem, + orchestrator.WithForce()) + + create := o.CertificateCreate("_any", osapi.CertificateCreateOpts{ + Name: certName, + Object: "osapi-example-ca.pem", + }).After(upload) + + list := o.CertificateList("_any").After(create) + + o.CertificateDelete("_any", certName).After(list) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var certs osapi.CertificateCAResult + if err := report.Decode("list-certificate", &certs); err == nil { + fmt.Printf("CA certificates (%d):\n", len(certs.Certificates)) + for _, c := range certs.Certificates { + fmt.Printf(" %s (source: %s)\n", c.Name, c.Source) + } + } +} diff --git a/examples/operations/go.mod b/examples/operations/go.mod index 98a8943..568ae81 100644 --- a/examples/operations/go.mod +++ b/examples/operations/go.mod @@ -6,7 +6,7 @@ replace github.com/osapi-io/osapi-orchestrator => ../.. require ( github.com/osapi-io/osapi-orchestrator v0.0.0-20260307062306-1d8135903db8 - github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 + github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 ) require ( @@ -29,9 +29,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.42.0 // indirect ) diff --git a/examples/operations/go.sum b/examples/operations/go.sum index 899a053..22ab6da 100644 --- a/examples/operations/go.sum +++ b/examples/operations/go.sum @@ -42,8 +42,8 @@ github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+Y github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 h1:myAfjno/+7oFGdhhi3GPxJnyY8aWAArfsf1BCvRTCdA= -github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150/go.mod h1:rM3VRcxtQ2by9hQpXFHJqMtZZdyaptp9weuICXBde5A= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 h1:xnr/b4Lx+kIYld0+nu/bllZfVFIiPYRow00ykf8tQtE= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616/go.mod h1:K8DSx5/jbgscnu51AJvw9KzWGYdY+L+mmQ6n45dCjwo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -56,12 +56,12 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/examples/operations/interface.go b/examples/operations/interface.go new file mode 100644 index 0000000..5ca5129 --- /dev/null +++ b/examples/operations/interface.go @@ -0,0 +1,90 @@ +// 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 network interface management: +// list interfaces, then get details on a specific one. +// +// Uses ContinueOnError since Netplan-based interface management +// is only available on Debian-family systems. +// +// DAG: +// +// list-interface +// └── get-interface +// +// Run with: OSAPI_TOKEN="" OSAPI_INTERFACE="eth0" go run interface.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + iface := os.Getenv("OSAPI_INTERFACE") + if iface == "" { + iface = "eth0" + } + + o := orchestrator.New(url, token) + + list := o.InterfaceList("_any").ContinueOnError() + + o.InterfaceGet("_any", iface).After(list).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var ifaces osapi.InterfaceListResult + if err := report.Decode("list-interface", &ifaces); err == nil { + fmt.Printf("Network interfaces (%d):\n", len(ifaces.Interfaces)) + for _, i := range ifaces.Interfaces { + fmt.Printf(" %s: ipv4=%s, state=%s, managed=%v\n", + i.Name, i.IPv4, i.State, i.Managed) + } + } else { + fmt.Println("Interface management requires Netplan (Debian-family only)") + } + + var detail osapi.InterfaceGetResult + if err := report.Decode("get-interface", &detail); err == nil && detail.Interface != nil { + i := detail.Interface + fmt.Printf("\nInterface %s: dhcp4=%v, mtu=%d, mac=%s\n", + i.Name, i.DHCP4, i.MTU, i.MAC) + } +} diff --git a/examples/operations/log.go b/examples/operations/log.go new file mode 100644 index 0000000..7092568 --- /dev/null +++ b/examples/operations/log.go @@ -0,0 +1,91 @@ +// 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 journal log querying: +// query recent logs, list available sources, and query by unit. +// +// All steps use ContinueOnError since log access may require +// systemd-journald on the target. +// +// DAG: +// +// query-log +// └── list-log-sources +// └── query-log-unit +// +// Run with: OSAPI_TOKEN="" go run log.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + lines := 10 + + o := orchestrator.New(url, token) + + query := o.LogQuery("_any", osapi.LogQueryOpts{ + Lines: &lines, + }).ContinueOnError() + + sources := o.LogSources("_any").After(query).ContinueOnError() + + o.LogQueryUnit("_any", "sshd", osapi.LogQueryOpts{ + Lines: &lines, + }).After(sources).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var logs osapi.LogEntryResult + if err := report.Decode("query-log", &logs); err == nil { + fmt.Printf("Recent log entries: %d\n", len(logs.Entries)) + for _, e := range logs.Entries { + fmt.Printf(" [%s] %s: %s\n", e.Priority, e.Unit, e.Message) + } + } else { + fmt.Println("Log operations require systemd-journald") + } + + var src osapi.LogSourceResult + if err := report.Decode("list-log-sources", &src); err == nil { + fmt.Printf("Log sources: %d units\n", len(src.Sources)) + } +} diff --git a/examples/operations/ntp.go b/examples/operations/ntp.go new file mode 100644 index 0000000..d7902cd --- /dev/null +++ b/examples/operations/ntp.go @@ -0,0 +1,87 @@ +// 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 NTP server management: +// create configuration, read status, update servers, then delete. +// +// Cleanup at the start ensures repeatability. +// +// DAG: +// +// create-ntp +// └── get-ntp +// └── update-ntp +// └── delete-ntp +// +// Run with: OSAPI_TOKEN="" go run ntp.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + // Cleanup any leftover NTP config from a previous run. + oc := orchestrator.New(url, token) + oc.NTPDelete("_any").ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Create → get → update → delete. + o := orchestrator.New(url, token) + + create := o.NTPCreate("_any", osapi.NtpCreateOpts{ + Servers: []string{"0.pool.ntp.org", "1.pool.ntp.org"}, + }) + + get := o.NTPGet("_any").After(create) + + update := o.NTPUpdate("_any", osapi.NtpUpdateOpts{ + Servers: []string{"time.google.com", "time.cloudflare.com"}, + }).After(get) + + o.NTPDelete("_any").After(update) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var ntp osapi.NtpStatusResult + if err := report.Decode("get-ntp", &ntp); err == nil { + fmt.Printf("NTP servers: %v (synced: %v)\n", ntp.Servers, ntp.Synchronized) + } +} diff --git a/examples/operations/package.go b/examples/operations/package.go new file mode 100644 index 0000000..e2090e1 --- /dev/null +++ b/examples/operations/package.go @@ -0,0 +1,89 @@ +// 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 package management: +// install a package, inspect it, list available updates, then remove it. +// +// Uses ContinueOnError since package operations require privileges +// and may not be available on all platforms. +// +// DAG: +// +// install-package +// └── get-package +// └── list-package-updates +// └── remove-package +// +// Run with: OSAPI_TOKEN="" go run package.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const pkg = "sl" + + // Cleanup any leftover package from a previous run. + oc := orchestrator.New(url, token) + oc.PackageRemove("_any", pkg).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Install → get → list updates → remove. + o := orchestrator.New(url, token) + + install := o.PackageInstall("_any", pkg).ContinueOnError() + + get := o.PackageGet("_any", pkg).After(install).ContinueOnError() + + updates := o.PackageListUpdates("_any").After(get).ContinueOnError() + + o.PackageRemove("_any", pkg).After(updates).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var info osapi.PackageInfoResult + if err := report.Decode("get-package", &info); err == nil && len(info.Packages) > 0 { + p := info.Packages[0] + fmt.Printf("Package %s: version=%s, status=%s\n", p.Name, p.Version, p.Status) + } else { + fmt.Println("Package operations require apt/dpkg (Debian-family only)") + } +} diff --git a/examples/operations/power.go b/examples/operations/power.go new file mode 100644 index 0000000..115606d --- /dev/null +++ b/examples/operations/power.go @@ -0,0 +1,77 @@ +// 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 power management with a delayed reboot. +// +// WARNING: This example will actually reboot the target host! +// It uses a delay to give time to cancel. Uses ContinueOnError +// since power operations require privileges. +// +// DAG: +// +// reboot +// +// Run with: OSAPI_TOKEN="" go run power.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + o := orchestrator.New(url, token) + + // Schedule a reboot with a 60-second delay. + // WARNING: this will reboot the target host. + o.PowerReboot("_any", osapi.PowerOpts{ + Delay: 60, + Message: "OSAPI orchestrator example reboot", + }).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var result osapi.PowerResult + if err := report.Decode("reboot", &result); err == nil { + fmt.Printf("Power %s: delay=%ds, changed=%v\n", + result.Action, result.Delay, result.Changed) + } else { + fmt.Println("Power operations require privileges") + } +} diff --git a/examples/operations/process.go b/examples/operations/process.go new file mode 100644 index 0000000..35801ae --- /dev/null +++ b/examples/operations/process.go @@ -0,0 +1,75 @@ +// 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 process management: +// list running processes and inspect PID 1. +// +// DAG: +// +// list-process +// └── get-process +// +// Run with: OSAPI_TOKEN="" go run process.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + o := orchestrator.New(url, token) + + list := o.ProcessList("_any") + + o.ProcessGet("_any", 1).After(list) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var procs osapi.ProcessInfoResult + if err := report.Decode("list-process", &procs); err == nil { + fmt.Printf("Running processes: %d\n", len(procs.Processes)) + } + + var init osapi.ProcessInfoResult + if err := report.Decode("get-process", &init); err == nil && len(init.Processes) > 0 { + p := init.Processes[0] + fmt.Printf("PID 1: %s (user: %s, state: %s)\n", p.Name, p.User, p.State) + } +} diff --git a/examples/operations/route.go b/examples/operations/route.go new file mode 100644 index 0000000..41a1bf9 --- /dev/null +++ b/examples/operations/route.go @@ -0,0 +1,83 @@ +// 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 network route management: +// list routes on an interface and get a specific route. +// +// Uses ContinueOnError since route management requires Netplan +// and may not be available on all platforms. +// +// DAG: +// +// list-route +// └── get-route +// +// Run with: OSAPI_TOKEN="" OSAPI_INTERFACE="eth0" go run route.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + iface := os.Getenv("OSAPI_INTERFACE") + if iface == "" { + iface = "eth0" + } + + o := orchestrator.New(url, token) + + list := o.RouteList("_any").ContinueOnError() + + o.RouteGet("_any", iface).After(list).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var routes osapi.RouteListResult + if err := report.Decode("list-route", &routes); err == nil { + fmt.Printf("Routes on %s (%d):\n", iface, len(routes.Routes)) + for _, r := range routes.Routes { + fmt.Printf(" %s via %s (metric: %d, managed: %v)\n", + r.Destination, r.Gateway, r.Metric, r.Managed) + } + } else { + fmt.Println("Route management requires Netplan (Debian-family only)") + } +} diff --git a/examples/operations/service.go b/examples/operations/service.go new file mode 100644 index 0000000..f56bb51 --- /dev/null +++ b/examples/operations/service.go @@ -0,0 +1,117 @@ +// 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 systemd service lifecycle management: +// upload a unit file, create the service, start, enable, then +// cleanup: disable, stop, delete. +// +// Cleanup at the start ensures repeatability. +// +// DAG: +// +// upload-file +// └── create-service +// └── start-service +// └── enable-service +// └── get-service +// └── disable-service +// └── stop-service +// └── delete-service +// +// Run with: OSAPI_TOKEN="" go run service.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const svcName = "osapi-example" + + unitFile := []byte(`[Unit] +Description=OSAPI Example Service + +[Service] +Type=oneshot +ExecStart=/bin/true +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +`) + + // Cleanup any leftover service from a previous run. + oc := orchestrator.New(url, token) + oc.ServiceDisable("_any", svcName).ContinueOnError() + oc.ServiceStop("_any", svcName).ContinueOnError() + oc.ServiceDelete("_any", svcName).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Upload → create → start → enable → get → disable → stop → delete. + o := orchestrator.New(url, token) + + upload := o.FileUpload("osapi-example.service", "raw", unitFile, + orchestrator.WithForce()) + + create := o.ServiceCreate("_any", osapi.ServiceCreateOpts{ + Name: svcName, + Object: "osapi-example.service", + }).After(upload) + + start := o.ServiceStart("_any", svcName).After(create) + + enable := o.ServiceEnable("_any", svcName).After(start) + + get := o.ServiceGet("_any", svcName).After(enable) + + disable := o.ServiceDisable("_any", svcName).After(get) + + stop := o.ServiceStop("_any", svcName).After(disable) + + o.ServiceDelete("_any", svcName).After(stop) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var svc osapi.ServiceGetResult + if err := report.Decode("get-service", &svc); err == nil && svc.Service != nil { + fmt.Printf("Service %q: status=%s, enabled=%v\n", + svc.Service.Name, svc.Service.Status, svc.Service.Enabled) + } +} diff --git a/examples/operations/sysctl.go b/examples/operations/sysctl.go new file mode 100644 index 0000000..a9d704b --- /dev/null +++ b/examples/operations/sysctl.go @@ -0,0 +1,90 @@ +// 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 sysctl parameter management: +// create a parameter, read it, update it, then delete it. +// +// Cleanup at the start ensures repeatability. +// +// DAG: +// +// create-sysctl +// └── get-sysctl +// └── update-sysctl +// └── delete-sysctl +// +// Run with: OSAPI_TOKEN="" go run sysctl.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const key = "net.ipv4.ip_forward" + + // Cleanup any leftover parameter from a previous run. + oc := orchestrator.New(url, token) + oc.SysctlDelete("_any", key).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Create → get → update → delete. + o := orchestrator.New(url, token) + + create := o.SysctlCreate("_any", osapi.SysctlCreateOpts{ + Key: key, + Value: "0", + }) + + get := o.SysctlGet("_any", key).After(create) + + update := o.SysctlUpdate("_any", key, osapi.SysctlUpdateOpts{ + Value: "1", + }).After(get) + + o.SysctlDelete("_any", key).After(update) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var entry osapi.SysctlEntryResult + if err := report.Decode("get-sysctl", &entry); err == nil { + fmt.Printf("Sysctl %s = %s\n", entry.Key, entry.Value) + } +} diff --git a/examples/operations/timezone.go b/examples/operations/timezone.go new file mode 100644 index 0000000..5dcbea2 --- /dev/null +++ b/examples/operations/timezone.go @@ -0,0 +1,73 @@ +// 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 timezone management: +// read the current timezone, then update it. +// +// Uses ContinueOnError since timezone changes require privileges. +// +// DAG: +// +// get-timezone +// └── update-timezone +// +// Run with: OSAPI_TOKEN="" go run timezone.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + o := orchestrator.New(url, token) + + get := o.TimezoneGet("_any") + + o.TimezoneUpdate("_any", osapi.TimezoneUpdateOpts{ + Timezone: "America/New_York", + }).After(get).ContinueOnError() + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var tz osapi.TimezoneResult + if err := report.Decode("get-timezone", &tz); err == nil { + fmt.Printf("Timezone: %s (UTC%s)\n", tz.Timezone, tz.UTCOffset) + } +} diff --git a/examples/operations/user.go b/examples/operations/user.go new file mode 100644 index 0000000..8568834 --- /dev/null +++ b/examples/operations/user.go @@ -0,0 +1,103 @@ +// 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 user account management: +// create a user, add an SSH key, list keys, remove the key, then +// delete the user. +// +// Cleanup at the start ensures repeatability. +// +// DAG: +// +// create-user +// └── add-ssh-key +// └── list-ssh-key +// └── remove-ssh-key +// └── delete-user +// +// Run with: OSAPI_TOKEN="" go run user.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const username = "osapi-example-user" + + // Example SSH public key (not a real key). + const sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample osapi-example" + + // Cleanup any leftover user from a previous run. + oc := orchestrator.New(url, token) + oc.UserDelete("_any", username).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // Create user → add key → list keys → remove key → delete user. + o := orchestrator.New(url, token) + + create := o.UserCreate("_any", osapi.UserCreateOpts{ + Name: username, + Shell: "/bin/bash", + }) + + addKey := o.UserAddKey("_any", username, osapi.SSHKeyAddOpts{ + Key: sshKey, + }).After(create) + + listKeys := o.UserListKeys("_any", username).After(addKey) + + // Use a placeholder fingerprint — in practice you would decode + // the key's fingerprint from list-ssh-key results. + removeKey := o.UserRemoveKey("_any", username, "SHA256:example"). + After(listKeys).ContinueOnError() + + o.UserDelete("_any", username).After(removeKey) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var keys osapi.SSHKeyInfoResult + if err := report.Decode("list-ssh-key", &keys); err == nil { + fmt.Printf("User %q has %d SSH key(s)\n", username, len(keys.Keys)) + for _, k := range keys.Keys { + fmt.Printf(" %s %s\n", k.Type, k.Fingerprint) + } + } +} diff --git a/go.mod b/go.mod index e37f895..4e9a4a7 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ tool ( require ( github.com/charmbracelet/lipgloss v1.1.0 - github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 + github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 github.com/stretchr/testify v1.11.1 ) @@ -32,6 +32,7 @@ require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect @@ -43,9 +44,11 @@ require ( github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect + github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect + github.com/avfs/avfs v0.35.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect @@ -57,8 +60,10 @@ require ( github.com/breml/errchkjson v0.4.1 // indirect github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect + github.com/caarlos0/go-version v0.2.2 // indirect github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect @@ -66,22 +71,36 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.19.0 // indirect github.com/fatih/structtag v1.2.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/ggwhite/go-masker/v2 v2.2.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -94,6 +113,7 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect @@ -106,12 +126,14 @@ require ( github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -124,8 +146,11 @@ require ( github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect + github.com/labstack/echo/v4 v4.15.1 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect github.com/ldez/gomoddirectives v0.8.0 // indirect @@ -133,8 +158,11 @@ require ( github.com/ldez/structtags v0.6.1 // indirect github.com/ldez/tagliatelle v0.7.2 // indirect github.com/ldez/usetesting v0.5.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect + github.com/lmittmann/tint v1.1.3 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect @@ -146,21 +174,36 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mgechev/revive v1.15.0 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect + github.com/nats-io/nats-server/v2 v2.12.6 // indirect + github.com/nats-io/nats.go v1.50.0 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/nxadm/tail v1.4.11 // indirect github.com/oapi-codegen/runtime v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/osapi-io/nats-client v0.0.0-20260322200309-c8bab331f583 // indirect + github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus-community/pro-bing v0.8.0 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect @@ -169,16 +212,20 @@ require ( github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/samber/slog-echo v1.21.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect github.com/segmentio/golines v0.13.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.5.1 // indirect @@ -196,12 +243,16 @@ require ( github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.1 // indirect github.com/uudashr/iface v1.4.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect @@ -209,15 +260,25 @@ require ( github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect @@ -225,11 +286,16 @@ require ( golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.43.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect diff --git a/go.sum b/go.sum index cd4945b..fd1fb44 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= @@ -57,12 +59,16 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE= +github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= +github.com/avfs/avfs v0.35.0 h1:dc0noSyEoVDtAUQlhHX0uRYBfI2aFbI8nF0XPtV/Ozw= +github.com/avfs/avfs v0.35.0/go.mod h1:LnzrUO5acMU5NCkohHcUN15YrnkxiJ/lRLQOZSp39ow= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -86,10 +92,14 @@ github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= +github.com/caarlos0/go-version v0.2.2 h1:5r+nlrg4H2wOVwWjqRqRRIRbZ7ytRmjC9xoMIP0a5kQ= +github.com/caarlos0/go-version v0.2.2/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= @@ -106,6 +116,10 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= @@ -121,14 +135,26 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -138,6 +164,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ggwhite/go-masker/v2 v2.2.0 h1:1lrBbY/Nej7rREu01Sf34EFpbN8aSNl04X17V5/Zle8= +github.com/ggwhite/go-masker/v2 v2.2.0/go.mod h1:Xky8hDSkBwV7pwCJvFsJaiFVwU9GCt0yIAx6yuAc6xY= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= @@ -147,6 +177,14 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -180,6 +218,8 @@ github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5W github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= @@ -204,9 +244,12 @@ github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqt github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -225,6 +268,8 @@ github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduw github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -253,6 +298,8 @@ github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+r github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -261,6 +308,10 @@ github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= +github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= +github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= @@ -275,10 +326,16 @@ github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYU github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= @@ -303,8 +360,12 @@ github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= @@ -313,6 +374,16 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= +github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= +github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= +github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= @@ -329,6 +400,14 @@ github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/osapi-io/nats-client v0.0.0-20260322200309-c8bab331f583 h1:WddbBQX9fVivF/08Vpev20z/k8k+ryzXRoyEt+OMEEw= +github.com/osapi-io/nats-client v0.0.0-20260322200309-c8bab331f583/go.mod h1:eqZafQKr+VWRTFsnky9Z1G3QtsZPOf1zXEzYdlegvbw= +github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848 h1:ELW1sTVBn5JIc17mHgd5fhpO3/7btaxJpxykG2Fe0U4= +github.com/osapi-io/nats-server v0.0.0-20260216201410-1f33dfc63848/go.mod h1:4rzeY9jiJF/+Ej4WNwqK5HQ2sflZrEs60GxQpg3Iya8= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -338,15 +417,23 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9 github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc= +github.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= @@ -363,9 +450,13 @@ github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74 github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150 h1:myAfjno/+7oFGdhhi3GPxJnyY8aWAArfsf1BCvRTCdA= github.com/retr0h/osapi v0.0.0-20260329025640-5e5b3c87c150/go.mod h1:rM3VRcxtQ2by9hQpXFHJqMtZZdyaptp9weuICXBde5A= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616 h1:xnr/b4Lx+kIYld0+nu/bllZfVFIiPYRow00ykf8tQtE= +github.com/retr0h/osapi v0.0.0-20260406175417-2d93f6ff0616/go.mod h1:K8DSx5/jbgscnu51AJvw9KzWGYdY+L+mmQ6n45dCjwo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -375,6 +466,10 @@ github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7 github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/slog-echo v1.21.0 h1:7qyzNeYTpbCbBrlF7C3XY1gsG5LHdrDVd0Ci9mSWRl0= +github.com/samber/slog-echo v1.21.0/go.mod h1:caG3zeXgrPRlGKaPVqyWG1MEc6nwrmtDjoLN/mc0PrM= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -389,6 +484,8 @@ github.com/segmentio/golines v0.13.0 h1:GfbpsxoF4eYuEZD3mxrlsN/XD30m6nOO4QLQj2JI github.com/segmentio/golines v0.13.0/go.mod h1:MMEi38dnJiyxqFZqFOqN14QMzWHzj/i0+L9Q2MsVr64= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= @@ -443,6 +540,10 @@ github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= @@ -455,6 +556,10 @@ github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= @@ -475,6 +580,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= @@ -489,12 +596,36 @@ go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDoj go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0 h1:0FKdyaoWXDmSCpQuv3m2UiJIRNxb1CK1mILy5QyKxc4= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.67.0/go.mod h1:IXtTS6zjKfM2yNRD9rWOS7SfIYGtuLGhL9ent5WX3Uk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -558,9 +689,11 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -576,6 +709,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -596,6 +730,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= @@ -618,6 +754,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/orchestrator/ops.go b/pkg/orchestrator/ops.go index 204dcc2..c83d3ca 100644 --- a/pkg/orchestrator/ops.go +++ b/pkg/orchestrator/ops.go @@ -44,6 +44,28 @@ func (o *Orchestrator) nextOpName( return prefix } +// commandError returns an error string for a command result. If the +// server set an explicit error, that takes precedence. Otherwise a +// non-zero exit code is treated as a failure so that guards like +// OnlyIfAnyHostFailed work naturally for command steps. +func commandError( + r osapi.CommandResult, +) string { + if r.Error != "" { + return r.Error + } + + if r.ExitCode != 0 { + return fmt.Sprintf("exit code %d", r.ExitCode) + } + + return "" +} + +// --------------------------------------------------------------------------- +// Health +// --------------------------------------------------------------------------- + // HealthCheck creates a health check step. func (o *Orchestrator) HealthCheck() *Step { name := o.nextOpName("health-check") @@ -66,6 +88,10 @@ func (o *Orchestrator) HealthCheck() *Step { return &Step{task: task} } +// --------------------------------------------------------------------------- +// Node: Hostname +// --------------------------------------------------------------------------- + // NodeHostnameGet creates a step that retrieves the hostname. func (o *Orchestrator) NodeHostnameGet( target string, @@ -78,7 +104,7 @@ func (o *Orchestrator) NodeHostnameGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Hostname(ctx, target) + resp, err := c.Hostname.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get hostname: %w", err) } @@ -99,6 +125,44 @@ func (o *Orchestrator) NodeHostnameGet( return &Step{task: task} } +// NodeHostnameUpdate creates a step that sets the system hostname. +func (o *Orchestrator) NodeHostnameUpdate( + target string, + hostname string, +) *Step { + name := o.nextOpName("update-hostname") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Hostname.Update(ctx, target, hostname) + if err != nil { + return nil, fmt.Errorf("update hostname: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.HostnameUpdateResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Node: Status, Uptime, Disk, Memory, Load, OS +// --------------------------------------------------------------------------- + // NodeStatusGet creates a step that retrieves node status. func (o *Orchestrator) NodeStatusGet( target string, @@ -111,7 +175,7 @@ func (o *Orchestrator) NodeStatusGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Status(ctx, target) + resp, err := c.Status.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get status: %w", err) } @@ -144,7 +208,7 @@ func (o *Orchestrator) NodeUptimeGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Uptime(ctx, target) + resp, err := c.Uptime.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get uptime: %w", err) } @@ -177,7 +241,7 @@ func (o *Orchestrator) NodeDiskGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Disk(ctx, target) + resp, err := c.Disk.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get disk: %w", err) } @@ -210,7 +274,7 @@ func (o *Orchestrator) NodeMemoryGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Memory(ctx, target) + resp, err := c.Memory.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get memory: %w", err) } @@ -243,7 +307,7 @@ func (o *Orchestrator) NodeLoadGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Load(ctx, target) + resp, err := c.Load.Get(ctx, target) if err != nil { return nil, fmt.Errorf("get load: %w", err) } @@ -264,6 +328,43 @@ func (o *Orchestrator) NodeLoadGet( return &Step{task: task} } +// NodeOSGet creates a step that retrieves OS information. +func (o *Orchestrator) NodeOSGet( + target string, +) *Step { + name := o.nextOpName("get-os") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.OS.Get(ctx, target) + if err != nil { + return nil, fmt.Errorf("get os: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.OSInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Network: DNS +// --------------------------------------------------------------------------- + // NetworkDNSGet creates a step that retrieves DNS configuration. func (o *Orchestrator) NetworkDNSGet( target string, @@ -277,7 +378,7 @@ func (o *Orchestrator) NetworkDNSGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.GetDNS(ctx, target, interfaceName) + resp, err := c.DNS.Get(ctx, target, interfaceName) if err != nil { return nil, fmt.Errorf("get dns: %w", err) } @@ -313,7 +414,9 @@ func (o *Orchestrator) NetworkDNSUpdate( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.UpdateDNS(ctx, target, interfaceName, servers, searchDomains) + resp, err := c.DNS.Update( + ctx, target, interfaceName, servers, searchDomains, false, + ) if err != nil { return nil, fmt.Errorf("update dns: %w", err) } @@ -334,6 +437,40 @@ func (o *Orchestrator) NetworkDNSUpdate( return &Step{task: task} } +// NetworkDNSDelete creates a step that deletes DNS configuration. +func (o *Orchestrator) NetworkDNSDelete( + target string, + interfaceName string, +) *Step { + name := o.nextOpName("delete-dns") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.DNS.Delete(ctx, target, interfaceName) + if err != nil { + return nil, fmt.Errorf("delete dns: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DNSDeleteResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + // NetworkPingDo creates a step that pings an address. func (o *Orchestrator) NetworkPingDo( target string, @@ -347,7 +484,7 @@ func (o *Orchestrator) NetworkPingDo( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Ping(ctx, target, address) + resp, err := c.Ping.Do(ctx, target, address) if err != nil { return nil, fmt.Errorf("ping: %w", err) } @@ -368,13 +505,15 @@ func (o *Orchestrator) NetworkPingDo( return &Step{task: task} } -// CommandExec creates a step that executes a command. -func (o *Orchestrator) CommandExec( +// --------------------------------------------------------------------------- +// Network: Interface +// --------------------------------------------------------------------------- + +// InterfaceList creates a step that lists network interfaces. +func (o *Orchestrator) InterfaceList( target string, - command string, - args ...string, ) *Step { - name := o.nextOpName(fmt.Sprintf("run-%s", filepath.Base(command))) + name := o.nextOpName("list-interface") task := o.plan.TaskFunc( name, @@ -382,22 +521,17 @@ func (o *Orchestrator) CommandExec( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Exec(ctx, osapi.ExecRequest{ - Command: command, - Args: args, - Target: target, - }) + resp, err := c.Interface.List(ctx, target) if err != nil { - return nil, fmt.Errorf("exec command: %w", err) + return nil, fmt.Errorf("list interfaces: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CommandResult) engine.HostResult { + func(r osapi.InterfaceListResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, - Error: commandError(r), + Error: r.Error, } }, ) @@ -407,17 +541,12 @@ func (o *Orchestrator) CommandExec( return &Step{task: task} } -// CommandShell creates a step that executes a shell command string. -func (o *Orchestrator) CommandShell( +// InterfaceGet creates a step that retrieves a specific network interface. +func (o *Orchestrator) InterfaceGet( target string, - command string, + ifaceName string, ) *Step { - shellName := strings.Fields(command) - if len(shellName) > 0 { - shellName[0] = filepath.Base(shellName[0]) - } - - name := o.nextOpName(fmt.Sprintf("shell-%s", shellName[0])) + name := o.nextOpName("get-interface") task := o.plan.TaskFunc( name, @@ -425,21 +554,17 @@ func (o *Orchestrator) CommandShell( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.Shell(ctx, osapi.ShellRequest{ - Command: command, - Target: target, - }) + resp, err := c.Interface.Get(ctx, target, ifaceName) if err != nil { - return nil, fmt.Errorf("shell command: %w", err) + return nil, fmt.Errorf("get interface: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CommandResult) engine.HostResult { + func(r osapi.InterfaceGetResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, - Error: commandError(r), + Error: r.Error, } }, ) @@ -449,34 +574,50 @@ func (o *Orchestrator) CommandShell( return &Step{task: task} } -// commandError returns an error string for a command result. If the -// server set an explicit error, that takes precedence. Otherwise a -// non-zero exit code is treated as a failure so that guards like -// OnlyIfAnyHostFailed work naturally for command steps. -func commandError( - r osapi.CommandResult, -) string { - if r.Error != "" { - return r.Error - } +// InterfaceCreate creates a step that creates a network interface +// configuration. +func (o *Orchestrator) InterfaceCreate( + target string, + ifaceName string, + opts osapi.InterfaceConfigOpts, +) *Step { + name := o.nextOpName("create-interface") - if r.ExitCode != 0 { - return fmt.Sprintf("exit code %d", r.ExitCode) - } + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Interface.Create(ctx, target, ifaceName, opts) + if err != nil { + return nil, fmt.Errorf("create interface: %w", err) + } - return "" + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.InterfaceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} } -// FileDeploy creates a step that deploys a file from the Object Store -// to the target agent's filesystem. The objectName must reference a -// file previously uploaded to the Object Store. ContentType should be -// "raw" for literal content or "template" for Go-template rendering -// with vars and agent facts. -func (o *Orchestrator) FileDeploy( +// InterfaceUpdate creates a step that updates a network interface +// configuration. +func (o *Orchestrator) InterfaceUpdate( target string, - opts osapi.FileDeployOpts, + ifaceName string, + opts osapi.InterfaceConfigOpts, ) *Step { - name := o.nextOpName("deploy-file") + name := o.nextOpName("update-interface") task := o.plan.TaskFunc( name, @@ -484,15 +625,13 @@ func (o *Orchestrator) FileDeploy( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - opts.Target = target - - resp, err := c.Node.FileDeploy(ctx, opts) + resp, err := c.Interface.Update(ctx, target, ifaceName, opts) if err != nil { - return nil, fmt.Errorf("deploy file: %w", err) + return nil, fmt.Errorf("update interface: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.FileDeployResult) engine.HostResult { + func(r osapi.InterfaceMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -507,14 +646,13 @@ func (o *Orchestrator) FileDeploy( return &Step{task: task} } -// FileStatusGet creates a step that checks the status of a deployed -// file on the target agent. Returns whether the file is in-sync, -// drifted, or missing compared to the expected state. -func (o *Orchestrator) FileStatusGet( +// InterfaceDelete creates a step that deletes a network interface +// configuration. +func (o *Orchestrator) InterfaceDelete( target string, - path string, + ifaceName string, ) *Step { - name := o.nextOpName("file-status") + name := o.nextOpName("delete-interface") task := o.plan.TaskFunc( name, @@ -522,13 +660,13 @@ func (o *Orchestrator) FileStatusGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.FileStatus(ctx, target, path) + resp, err := c.Interface.Delete(ctx, target, ifaceName) if err != nil { - return nil, fmt.Errorf("file status: %w", err) + return nil, fmt.Errorf("delete interface: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.FileStatusResult) engine.HostResult { + func(r osapi.InterfaceMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -543,13 +681,15 @@ func (o *Orchestrator) FileStatusGet( return &Step{task: task} } -// FileUndeploy creates a step that removes a previously deployed file -// from the target agent's filesystem. -func (o *Orchestrator) FileUndeploy( +// --------------------------------------------------------------------------- +// Network: Route +// --------------------------------------------------------------------------- + +// RouteList creates a step that lists network routes. +func (o *Orchestrator) RouteList( target string, - path string, ) *Step { - name := o.nextOpName("undeploy-file") + name := o.nextOpName("list-route") task := o.plan.TaskFunc( name, @@ -557,20 +697,16 @@ func (o *Orchestrator) FileUndeploy( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.FileUndeploy(ctx, osapi.FileUndeployOpts{ - Target: target, - Path: path, - }) + resp, err := c.Route.List(ctx, target) if err != nil { - return nil, fmt.Errorf("undeploy file: %w", err) + return nil, fmt.Errorf("list routes: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.FileUndeployResult) engine.HostResult { + func(r osapi.RouteListResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -581,99 +717,2174 @@ func (o *Orchestrator) FileUndeploy( return &Step{task: task} } -// FileUpload creates a step that uploads file content to the Object -// Store via the OSAPI REST API. Returns the object name that can be -// used in subsequent FileDeploy steps. This is a convenience wrapper -// that uses TaskFunc to call the file upload API directly. By default -// the upload is idempotent — the SDK compares SHA-256 digests and -// skips the upload when content is unchanged. Pass WithForce to always -// upload regardless of content changes. -func (o *Orchestrator) FileUpload( - name string, - contentType string, - data []byte, - opts ...UploadOption, +// RouteGet creates a step that retrieves routes for a specific interface. +func (o *Orchestrator) RouteGet( + target string, + interfaceName string, ) *Step { - cfg := &uploadConfig{} - for _, opt := range opts { - opt(cfg) - } - - taskName := o.nextOpName("upload-file") + name := o.nextOpName("get-route") task := o.plan.TaskFunc( - taskName, + name, func( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - var uploadOpts []osapi.UploadOption - if cfg.force { - uploadOpts = append(uploadOpts, osapi.WithForce()) + resp, err := c.Route.Get(ctx, target, interfaceName) + if err != nil { + return nil, fmt.Errorf("get route: %w", err) } - resp, err := c.File.Upload( - ctx, - name, - contentType, - bytes.NewReader(data), - uploadOpts..., + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.RouteGetResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, ) + }, + ) + + return &Step{task: task} +} + +// RouteCreate creates a step that creates route configuration. +func (o *Orchestrator) RouteCreate( + target string, + interfaceName string, + opts osapi.RouteConfigOpts, +) *Step { + name := o.nextOpName("create-route") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Route.Create(ctx, target, interfaceName, opts) + if err != nil { + return nil, fmt.Errorf("create route: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.RouteMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// RouteUpdate creates a step that updates route configuration. +func (o *Orchestrator) RouteUpdate( + target string, + interfaceName string, + opts osapi.RouteConfigOpts, +) *Step { + name := o.nextOpName("update-route") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Route.Update(ctx, target, interfaceName, opts) + if err != nil { + return nil, fmt.Errorf("update route: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.RouteMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// RouteDelete creates a step that deletes route configuration. +func (o *Orchestrator) RouteDelete( + target string, + interfaceName string, +) *Step { + name := o.nextOpName("delete-route") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Route.Delete(ctx, target, interfaceName) + if err != nil { + return nil, fmt.Errorf("delete route: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.RouteMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Command +// --------------------------------------------------------------------------- + +// CommandExec creates a step that executes a command. +func (o *Orchestrator) CommandExec( + target string, + command string, + args ...string, +) *Step { + name := o.nextOpName(fmt.Sprintf("run-%s", filepath.Base(command))) + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Command.Exec(ctx, osapi.ExecRequest{ + Command: command, + Args: args, + Target: target, + }) + if err != nil { + return nil, fmt.Errorf("exec command: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CommandResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: commandError(r), + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// CommandShell creates a step that executes a shell command string. +func (o *Orchestrator) CommandShell( + target string, + command string, +) *Step { + shellName := strings.Fields(command) + if len(shellName) > 0 { + shellName[0] = filepath.Base(shellName[0]) + } + + name := o.nextOpName(fmt.Sprintf("shell-%s", shellName[0])) + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Command.Shell(ctx, osapi.ShellRequest{ + Command: command, + Target: target, + }) + if err != nil { + return nil, fmt.Errorf("shell command: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CommandResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: commandError(r), + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// File +// --------------------------------------------------------------------------- + +// FileDeploy creates a step that deploys a file from the Object Store +// to the target agent's filesystem. The objectName must reference a +// file previously uploaded to the Object Store. ContentType should be +// "raw" for literal content or "template" for Go-template rendering +// with vars and agent facts. +func (o *Orchestrator) FileDeploy( + target string, + opts osapi.FileDeployOpts, +) *Step { + name := o.nextOpName("deploy-file") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + opts.Target = target + + resp, err := c.FileDeploy.Deploy(ctx, opts) + if err != nil { + return nil, fmt.Errorf("deploy file: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.FileDeployResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// FileStatusGet creates a step that checks the status of a deployed +// file on the target agent. Returns whether the file is in-sync, +// drifted, or missing compared to the expected state. +func (o *Orchestrator) FileStatusGet( + target string, + path string, +) *Step { + name := o.nextOpName("file-status") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.FileDeploy.Status(ctx, target, path) + if err != nil { + return nil, fmt.Errorf("file status: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.FileStatusResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// FileUndeploy creates a step that removes a previously deployed file +// from the target agent's filesystem. +func (o *Orchestrator) FileUndeploy( + target string, + path string, +) *Step { + name := o.nextOpName("undeploy-file") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.FileDeploy.Undeploy(ctx, osapi.FileUndeployOpts{ + Target: target, + Path: path, + }) + if err != nil { + return nil, fmt.Errorf("undeploy file: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.FileUndeployResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// FileUpload creates a step that uploads file content to the Object +// Store via the OSAPI REST API. Returns the object name that can be +// used in subsequent FileDeploy steps. This is a convenience wrapper +// that uses TaskFunc to call the file upload API directly. By default +// the upload is idempotent — the SDK compares SHA-256 digests and +// skips the upload when content is unchanged. Pass WithForce to always +// upload regardless of content changes. +func (o *Orchestrator) FileUpload( + name string, + contentType string, + data []byte, + opts ...UploadOption, +) *Step { + cfg := &uploadConfig{} + for _, opt := range opts { + opt(cfg) + } + + taskName := o.nextOpName("upload-file") + + task := o.plan.TaskFunc( + taskName, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + var uploadOpts []osapi.UploadOption + if cfg.force { + uploadOpts = append(uploadOpts, osapi.WithForce()) + } + + resp, err := c.File.Upload( + ctx, + name, + contentType, + bytes.NewReader(data), + uploadOpts..., + ) + if err != nil { + return nil, fmt.Errorf("upload file %s: %w", name, err) + } + + return &engine.Result{ + Changed: resp.Data.Changed, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// FileChanged creates a step that checks whether local content differs +// from the version stored in the Object Store. Computes SHA-256 locally +// and compares against the stored hash. Pairs with OnlyIfChanged to +// skip uploads when content is unchanged. +func (o *Orchestrator) FileChanged( + name string, + data []byte, +) *Step { + taskName := o.nextOpName("check-file") + + task := o.plan.TaskFunc( + taskName, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.File.Changed( + ctx, + name, + bytes.NewReader(data), + ) + if err != nil { + return nil, fmt.Errorf("check file %s: %w", name, err) + } + + return &engine.Result{ + Changed: resp.Data.Changed, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Docker +// --------------------------------------------------------------------------- + +// DockerPull creates a step that pulls a Docker image on the target host. +func (o *Orchestrator) DockerPull( + target string, + opts osapi.DockerPullOpts, +) *Step { + name := o.nextOpName("docker-pull") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Pull(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("docker pull: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerPullResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerCreate creates a step that creates a new container on the target host. +func (o *Orchestrator) DockerCreate( + target string, + opts osapi.DockerCreateOpts, +) *Step { + name := o.nextOpName("docker-create") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("docker create: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerStart creates a step that starts a stopped container on the target host. +func (o *Orchestrator) DockerStart( + target string, + id string, +) *Step { + name := o.nextOpName("docker-start") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Start(ctx, target, id) + if err != nil { + return nil, fmt.Errorf("docker start: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerActionResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerStop creates a step that stops a running container on the target host. +func (o *Orchestrator) DockerStop( + target string, + id string, + opts osapi.DockerStopOpts, +) *Step { + name := o.nextOpName("docker-stop") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Stop(ctx, target, id, opts) + if err != nil { + return nil, fmt.Errorf("docker stop: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerActionResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerRemove creates a step that removes a container from the target host. +func (o *Orchestrator) DockerRemove( + target string, + id string, + params *osapi.DockerRemoveParams, +) *Step { + name := o.nextOpName("docker-remove") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Remove(ctx, target, id, params) + if err != nil { + return nil, fmt.Errorf("docker remove: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerActionResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerExec creates a step that executes a command inside a running container. +func (o *Orchestrator) DockerExec( + target string, + id string, + opts osapi.DockerExecOpts, +) *Step { + name := o.nextOpName("docker-exec") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Exec(ctx, target, id, opts) + if err != nil { + return nil, fmt.Errorf("docker exec: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerExecResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerInspect creates a step that retrieves detailed info about a container. +func (o *Orchestrator) DockerInspect( + target string, + id string, +) *Step { + name := o.nextOpName("docker-inspect") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.Inspect(ctx, target, id) + if err != nil { + return nil, fmt.Errorf("docker inspect: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerDetailResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerList creates a step that lists containers on the target host. +func (o *Orchestrator) DockerList( + target string, + params *osapi.DockerListParams, +) *Step { + name := o.nextOpName("docker-list") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.List(ctx, target, params) + if err != nil { + return nil, fmt.Errorf("docker list: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerListResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// DockerImageRemove creates a step that removes a container image +// from the target host. +func (o *Orchestrator) DockerImageRemove( + target string, + imageName string, + params *osapi.DockerImageRemoveParams, +) *Step { + name := o.nextOpName("docker-image-remove") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Docker.ImageRemove( + ctx, + target, + imageName, + params, + ) + if err != nil { + return nil, fmt.Errorf("docker image remove: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.DockerActionResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Cron +// --------------------------------------------------------------------------- + +// CronList creates a step that lists cron entries on the target host. +func (o *Orchestrator) CronList( + target string, +) *Step { + name := o.nextOpName("list-cron") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Cron.List(ctx, target) + if err != nil { + return nil, fmt.Errorf("list cron: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CronEntryResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: false, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// CronGet creates a step that retrieves a specific cron entry on the target host. +func (o *Orchestrator) CronGet( + target string, + entryName string, +) *Step { + name := o.nextOpName("get-cron") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Cron.Get(ctx, target, entryName) + if err != nil { + return nil, fmt.Errorf("get cron: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CronEntryResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: false, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// CronCreate creates a step that creates a new cron entry on the target host. +func (o *Orchestrator) CronCreate( + target string, + opts osapi.CronCreateOpts, +) *Step { + name := o.nextOpName("create-cron") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Cron.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("create cron: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CronMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// CronUpdate creates a step that updates an existing cron entry on the target host. +func (o *Orchestrator) CronUpdate( + target string, + entryName string, + opts osapi.CronUpdateOpts, +) *Step { + name := o.nextOpName("update-cron") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Cron.Update(ctx, target, entryName, opts) + if err != nil { + return nil, fmt.Errorf("update cron: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CronMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// CronDelete creates a step that deletes a cron entry on the target host. +func (o *Orchestrator) CronDelete( + target string, + entryName string, +) *Step { + name := o.nextOpName("delete-cron") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Cron.Delete(ctx, target, entryName) + if err != nil { + return nil, fmt.Errorf("delete cron: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.CronMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Agent +// --------------------------------------------------------------------------- + +// AgentList creates a step that lists all active agents with their facts. +func (o *Orchestrator) AgentList() *Step { + name := o.nextOpName("list-agents") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Agent.List(ctx) + if err != nil { + return nil, fmt.Errorf("list agents: %w", err) + } + + return &engine.Result{ + Changed: false, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// AgentGet creates a step that retrieves detailed info about a specific agent. +func (o *Orchestrator) AgentGet( + hostname string, +) *Step { + name := o.nextOpName("get-agent") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Agent.Get(ctx, hostname) + if err != nil { + return nil, fmt.Errorf("get agent %s: %w", hostname, err) + } + + return &engine.Result{ + Changed: false, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// AgentDrain creates a step that drains an agent, preventing it from +// accepting new jobs. +func (o *Orchestrator) AgentDrain( + hostname string, +) *Step { + name := o.nextOpName("drain-agent") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Agent.Drain(ctx, hostname) + if err != nil { + return nil, fmt.Errorf("drain agent %s: %w", hostname, err) + } + + return &engine.Result{ + Changed: false, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// AgentUndrain creates a step that undrains an agent, allowing it to accept +// new jobs again. +func (o *Orchestrator) AgentUndrain( + hostname string, +) *Step { + name := o.nextOpName("undrain-agent") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Agent.Undrain(ctx, hostname) + if err != nil { + return nil, fmt.Errorf("undrain agent %s: %w", hostname, err) + } + + return &engine.Result{ + Changed: false, + Data: engine.StructToMap(resp.Data), + }, nil + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Sysctl +// --------------------------------------------------------------------------- + +// SysctlList creates a step that lists managed sysctl parameters. +func (o *Orchestrator) SysctlList( + target string, +) *Step { + name := o.nextOpName("list-sysctl") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Sysctl.List(ctx, target) + if err != nil { + return nil, fmt.Errorf("list sysctl: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SysctlEntryResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// SysctlGet creates a step that retrieves a sysctl parameter by key. +func (o *Orchestrator) SysctlGet( + target string, + key string, +) *Step { + name := o.nextOpName("get-sysctl") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Sysctl.Get(ctx, target, key) + if err != nil { + return nil, fmt.Errorf("get sysctl: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SysctlEntryResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// SysctlCreate creates a step that creates a sysctl parameter. +func (o *Orchestrator) SysctlCreate( + target string, + opts osapi.SysctlCreateOpts, +) *Step { + name := o.nextOpName("create-sysctl") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Sysctl.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("create sysctl: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SysctlMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// SysctlUpdate creates a step that updates a sysctl parameter. +func (o *Orchestrator) SysctlUpdate( + target string, + key string, + opts osapi.SysctlUpdateOpts, +) *Step { + name := o.nextOpName("update-sysctl") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Sysctl.Update(ctx, target, key, opts) + if err != nil { + return nil, fmt.Errorf("update sysctl: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SysctlMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// SysctlDelete creates a step that deletes a sysctl parameter. +func (o *Orchestrator) SysctlDelete( + target string, + key string, +) *Step { + name := o.nextOpName("delete-sysctl") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Sysctl.Delete(ctx, target, key) + if err != nil { + return nil, fmt.Errorf("delete sysctl: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SysctlMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// NTP +// --------------------------------------------------------------------------- + +// NTPGet creates a step that retrieves NTP status. +func (o *Orchestrator) NTPGet( + target string, +) *Step { + name := o.nextOpName("get-ntp") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.NTP.Get(ctx, target) + if err != nil { + return nil, fmt.Errorf("get ntp: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.NtpStatusResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// NTPCreate creates a step that creates NTP configuration. +func (o *Orchestrator) NTPCreate( + target string, + opts osapi.NtpCreateOpts, +) *Step { + name := o.nextOpName("create-ntp") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.NTP.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("create ntp: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.NtpMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// NTPUpdate creates a step that updates NTP configuration. +func (o *Orchestrator) NTPUpdate( + target string, + opts osapi.NtpUpdateOpts, +) *Step { + name := o.nextOpName("update-ntp") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.NTP.Update(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("update ntp: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.NtpMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// NTPDelete creates a step that deletes NTP configuration. +func (o *Orchestrator) NTPDelete( + target string, +) *Step { + name := o.nextOpName("delete-ntp") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.NTP.Delete(ctx, target) + if err != nil { + return nil, fmt.Errorf("delete ntp: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.NtpMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Timezone +// --------------------------------------------------------------------------- + +// TimezoneGet creates a step that retrieves the system timezone. +func (o *Orchestrator) TimezoneGet( + target string, +) *Step { + name := o.nextOpName("get-timezone") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Timezone.Get(ctx, target) + if err != nil { + return nil, fmt.Errorf("get timezone: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.TimezoneResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// TimezoneUpdate creates a step that sets the system timezone. +func (o *Orchestrator) TimezoneUpdate( + target string, + opts osapi.TimezoneUpdateOpts, +) *Step { + name := o.nextOpName("update-timezone") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Timezone.Update(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("update timezone: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.TimezoneMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Service +// --------------------------------------------------------------------------- + +// ServiceList creates a step that lists services on the target host. +func (o *Orchestrator) ServiceList( + target string, +) *Step { + name := o.nextOpName("list-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.List(ctx, target) + if err != nil { + return nil, fmt.Errorf("list services: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceGet creates a step that retrieves a specific service. +func (o *Orchestrator) ServiceGet( + target string, + serviceName string, +) *Step { + name := o.nextOpName("get-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Get(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("get service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceGetResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceCreate creates a step that creates a service unit file. +func (o *Orchestrator) ServiceCreate( + target string, + opts osapi.ServiceCreateOpts, +) *Step { + name := o.nextOpName("create-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("create service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceUpdate creates a step that updates a service unit file. +func (o *Orchestrator) ServiceUpdate( + target string, + serviceName string, + opts osapi.ServiceUpdateOpts, +) *Step { + name := o.nextOpName("update-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Update(ctx, target, serviceName, opts) + if err != nil { + return nil, fmt.Errorf("update service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceDelete creates a step that deletes a service unit file. +func (o *Orchestrator) ServiceDelete( + target string, + serviceName string, +) *Step { + name := o.nextOpName("delete-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Delete(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("delete service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceStart creates a step that starts a service. +func (o *Orchestrator) ServiceStart( + target string, + serviceName string, +) *Step { + name := o.nextOpName("start-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Start(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("start service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceStop creates a step that stops a service. +func (o *Orchestrator) ServiceStop( + target string, + serviceName string, +) *Step { + name := o.nextOpName("stop-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Stop(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("stop service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceRestart creates a step that restarts a service. +func (o *Orchestrator) ServiceRestart( + target string, + serviceName string, +) *Step { + name := o.nextOpName("restart-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Restart(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("restart service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceEnable creates a step that enables a service to start on boot. +func (o *Orchestrator) ServiceEnable( + target string, + serviceName string, +) *Step { + name := o.nextOpName("enable-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Enable(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("enable service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// ServiceDisable creates a step that disables a service from starting on boot. +func (o *Orchestrator) ServiceDisable( + target string, + serviceName string, +) *Step { + name := o.nextOpName("disable-service") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Service.Disable(ctx, target, serviceName) + if err != nil { + return nil, fmt.Errorf("disable service: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.ServiceMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// Package +// --------------------------------------------------------------------------- + +// PackageList creates a step that lists installed packages. +func (o *Orchestrator) PackageList( + target string, +) *Step { + name := o.nextOpName("list-package") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.List(ctx, target) + if err != nil { + return nil, fmt.Errorf("list packages: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// PackageGet creates a step that retrieves a specific package. +func (o *Orchestrator) PackageGet( + target string, + pkgName string, +) *Step { + name := o.nextOpName("get-package") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.Get(ctx, target, pkgName) + if err != nil { + return nil, fmt.Errorf("get package: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// PackageInstall creates a step that installs a package. +func (o *Orchestrator) PackageInstall( + target string, + pkgName string, +) *Step { + name := o.nextOpName("install-package") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.Install(ctx, target, pkgName) + if err != nil { + return nil, fmt.Errorf("install package: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// PackageRemove creates a step that removes a package. +func (o *Orchestrator) PackageRemove( + target string, + pkgName string, +) *Step { + name := o.nextOpName("remove-package") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.Remove(ctx, target, pkgName) + if err != nil { + return nil, fmt.Errorf("remove package: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// PackageUpdate creates a step that updates all packages. +func (o *Orchestrator) PackageUpdate( + target string, +) *Step { + name := o.nextOpName("update-package") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.Update(ctx, target) + if err != nil { + return nil, fmt.Errorf("update packages: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// PackageListUpdates creates a step that lists available package updates. +func (o *Orchestrator) PackageListUpdates( + target string, +) *Step { + name := o.nextOpName("list-package-updates") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.Package.ListUpdates(ctx, target) + if err != nil { + return nil, fmt.Errorf("list package updates: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.PackageUpdateResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// --------------------------------------------------------------------------- +// User +// --------------------------------------------------------------------------- + +// UserList creates a step that lists user accounts. +func (o *Orchestrator) UserList( + target string, +) *Step { + name := o.nextOpName("list-user") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.User.List(ctx, target) + if err != nil { + return nil, fmt.Errorf("list users: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// UserGet creates a step that retrieves a specific user account. +func (o *Orchestrator) UserGet( + target string, + username string, +) *Step { + name := o.nextOpName("get-user") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.User.Get(ctx, target, username) + if err != nil { + return nil, fmt.Errorf("get user: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// UserCreate creates a step that creates a user account. +func (o *Orchestrator) UserCreate( + target string, + opts osapi.UserCreateOpts, +) *Step { + name := o.nextOpName("create-user") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.User.Create(ctx, target, opts) + if err != nil { + return nil, fmt.Errorf("create user: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// UserUpdate creates a step that updates a user account. +func (o *Orchestrator) UserUpdate( + target string, + username string, + opts osapi.UserUpdateOpts, +) *Step { + name := o.nextOpName("update-user") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.User.Update(ctx, target, username, opts) + if err != nil { + return nil, fmt.Errorf("update user: %w", err) + } + + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) + }, + ) + + return &Step{task: task} +} + +// UserDelete creates a step that deletes a user account. +func (o *Orchestrator) UserDelete( + target string, + username string, +) *Step { + name := o.nextOpName("delete-user") + + task := o.plan.TaskFunc( + name, + func( + ctx context.Context, + c *osapi.Client, + ) (*engine.Result, error) { + resp, err := c.User.Delete(ctx, target, username) if err != nil { - return nil, fmt.Errorf("upload file %s: %w", name, err) + return nil, fmt.Errorf("delete user: %w", err) } - return &engine.Result{ - Changed: resp.Data.Changed, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// FileChanged creates a step that checks whether local content differs -// from the version stored in the Object Store. Computes SHA-256 locally -// and compares against the stored hash. Pairs with OnlyIfChanged to -// skip uploads when content is unchanged. -func (o *Orchestrator) FileChanged( - name string, - data []byte, +// UserListKeys creates a step that lists SSH authorized keys for a user. +func (o *Orchestrator) UserListKeys( + target string, + username string, ) *Step { - taskName := o.nextOpName("check-file") + name := o.nextOpName("list-ssh-key") task := o.plan.TaskFunc( - taskName, + name, func( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.File.Changed( - ctx, - name, - bytes.NewReader(data), - ) + resp, err := c.User.ListKeys(ctx, target, username) if err != nil { - return nil, fmt.Errorf("check file %s: %w", name, err) + return nil, fmt.Errorf("list ssh keys: %w", err) } - return &engine.Result{ - Changed: resp.Data.Changed, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.SSHKeyInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// NodeHostnameUpdate creates a step that sets the system hostname. -func (o *Orchestrator) NodeHostnameUpdate( +// UserAddKey creates a step that adds an SSH authorized key for a user. +func (o *Orchestrator) UserAddKey( target string, - hostname string, + username string, + opts osapi.SSHKeyAddOpts, ) *Step { - name := o.nextOpName("update-hostname") + name := o.nextOpName("add-ssh-key") task := o.plan.TaskFunc( name, @@ -681,13 +2892,13 @@ func (o *Orchestrator) NodeHostnameUpdate( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.UpdateHostname(ctx, target, hostname) + resp, err := c.User.AddKey(ctx, target, username, opts) if err != nil { - return nil, fmt.Errorf("update hostname: %w", err) + return nil, fmt.Errorf("add ssh key: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.HostnameUpdateResult) engine.HostResult { + func(r osapi.SSHKeyMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -702,11 +2913,13 @@ func (o *Orchestrator) NodeHostnameUpdate( return &Step{task: task} } -// NodeOSGet creates a step that retrieves OS information. -func (o *Orchestrator) NodeOSGet( +// UserRemoveKey creates a step that removes an SSH authorized key for a user. +func (o *Orchestrator) UserRemoveKey( target string, + username string, + fingerprint string, ) *Step { - name := o.nextOpName("get-os") + name := o.nextOpName("remove-ssh-key") task := o.plan.TaskFunc( name, @@ -714,13 +2927,13 @@ func (o *Orchestrator) NodeOSGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Node.OS(ctx, target) + resp, err := c.User.RemoveKey(ctx, target, username, fingerprint) if err != nil { - return nil, fmt.Errorf("get os: %w", err) + return nil, fmt.Errorf("remove ssh key: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.OSInfoResult) engine.HostResult { + func(r osapi.SSHKeyMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -735,9 +2948,13 @@ func (o *Orchestrator) NodeOSGet( return &Step{task: task} } -// AgentList creates a step that lists all active agents with their facts. -func (o *Orchestrator) AgentList() *Step { - name := o.nextOpName("list-agents") +// UserChangePassword creates a step that changes a user's password. +func (o *Orchestrator) UserChangePassword( + target string, + username string, + password string, +) *Step { + name := o.nextOpName("change-password") task := o.plan.TaskFunc( name, @@ -745,26 +2962,36 @@ func (o *Orchestrator) AgentList() *Step { ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Agent.List(ctx) + resp, err := c.User.ChangePassword(ctx, target, username, password) if err != nil { - return nil, fmt.Errorf("list agents: %w", err) + return nil, fmt.Errorf("change password: %w", err) } - return &engine.Result{ - Changed: false, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.UserMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// AgentGet creates a step that retrieves detailed info about a specific agent. -func (o *Orchestrator) AgentGet( - hostname string, +// --------------------------------------------------------------------------- +// Group +// --------------------------------------------------------------------------- + +// GroupList creates a step that lists groups. +func (o *Orchestrator) GroupList( + target string, ) *Step { - name := o.nextOpName("get-agent") + name := o.nextOpName("list-group") task := o.plan.TaskFunc( name, @@ -772,27 +2999,32 @@ func (o *Orchestrator) AgentGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Agent.Get(ctx, hostname) + resp, err := c.Group.List(ctx, target) if err != nil { - return nil, fmt.Errorf("get agent %s: %w", hostname, err) + return nil, fmt.Errorf("list groups: %w", err) } - return &engine.Result{ - Changed: false, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.GroupInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// AgentDrain creates a step that drains an agent, preventing it from -// accepting new jobs. -func (o *Orchestrator) AgentDrain( - hostname string, +// GroupGet creates a step that retrieves a specific group. +func (o *Orchestrator) GroupGet( + target string, + groupName string, ) *Step { - name := o.nextOpName("drain-agent") + name := o.nextOpName("get-group") task := o.plan.TaskFunc( name, @@ -800,27 +3032,32 @@ func (o *Orchestrator) AgentDrain( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Agent.Drain(ctx, hostname) + resp, err := c.Group.Get(ctx, target, groupName) if err != nil { - return nil, fmt.Errorf("drain agent %s: %w", hostname, err) + return nil, fmt.Errorf("get group: %w", err) } - return &engine.Result{ - Changed: false, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.GroupInfoResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// AgentUndrain creates a step that undrains an agent, allowing it to accept -// new jobs again. -func (o *Orchestrator) AgentUndrain( - hostname string, +// GroupCreate creates a step that creates a group. +func (o *Orchestrator) GroupCreate( + target string, + opts osapi.GroupCreateOpts, ) *Step { - name := o.nextOpName("undrain-agent") + name := o.nextOpName("create-group") task := o.plan.TaskFunc( name, @@ -828,27 +3065,34 @@ func (o *Orchestrator) AgentUndrain( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Agent.Undrain(ctx, hostname) + resp, err := c.Group.Create(ctx, target, opts) if err != nil { - return nil, fmt.Errorf("undrain agent %s: %w", hostname, err) + return nil, fmt.Errorf("create group: %w", err) } - return &engine.Result{ - Changed: false, - Data: engine.StructToMap(resp.Data), - }, nil + return engine.CollectionResult(resp.Data, resp.RawJSON(), + func(r osapi.GroupMutationResult) engine.HostResult { + return engine.HostResult{ + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, + } + }, + ) }, ) return &Step{task: task} } -// DockerPull creates a step that pulls a Docker image on the target host. -func (o *Orchestrator) DockerPull( +// GroupUpdate creates a step that updates a group. +func (o *Orchestrator) GroupUpdate( target string, - opts osapi.DockerPullOpts, + groupName string, + opts osapi.GroupUpdateOpts, ) *Step { - name := o.nextOpName("docker-pull") + name := o.nextOpName("update-group") task := o.plan.TaskFunc( name, @@ -856,13 +3100,13 @@ func (o *Orchestrator) DockerPull( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Pull(ctx, target, opts) + resp, err := c.Group.Update(ctx, target, groupName, opts) if err != nil { - return nil, fmt.Errorf("docker pull: %w", err) + return nil, fmt.Errorf("update group: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerPullResult) engine.HostResult { + func(r osapi.GroupMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -877,12 +3121,12 @@ func (o *Orchestrator) DockerPull( return &Step{task: task} } -// DockerCreate creates a step that creates a new container on the target host. -func (o *Orchestrator) DockerCreate( +// GroupDelete creates a step that deletes a group. +func (o *Orchestrator) GroupDelete( target string, - opts osapi.DockerCreateOpts, + groupName string, ) *Step { - name := o.nextOpName("docker-create") + name := o.nextOpName("delete-group") task := o.plan.TaskFunc( name, @@ -890,13 +3134,13 @@ func (o *Orchestrator) DockerCreate( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Create(ctx, target, opts) + resp, err := c.Group.Delete(ctx, target, groupName) if err != nil { - return nil, fmt.Errorf("docker create: %w", err) + return nil, fmt.Errorf("delete group: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerResult) engine.HostResult { + func(r osapi.GroupMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -911,12 +3155,15 @@ func (o *Orchestrator) DockerCreate( return &Step{task: task} } -// DockerStart creates a step that starts a stopped container on the target host. -func (o *Orchestrator) DockerStart( +// --------------------------------------------------------------------------- +// Certificate +// --------------------------------------------------------------------------- + +// CertificateList creates a step that lists CA certificates. +func (o *Orchestrator) CertificateList( target string, - id string, ) *Step { - name := o.nextOpName("docker-start") + name := o.nextOpName("list-certificate") task := o.plan.TaskFunc( name, @@ -924,17 +3171,16 @@ func (o *Orchestrator) DockerStart( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Start(ctx, target, id) + resp, err := c.Certificate.List(ctx, target) if err != nil { - return nil, fmt.Errorf("docker start: %w", err) + return nil, fmt.Errorf("list certificates: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerActionResult) engine.HostResult { + func(r osapi.CertificateCAResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -945,13 +3191,12 @@ func (o *Orchestrator) DockerStart( return &Step{task: task} } -// DockerStop creates a step that stops a running container on the target host. -func (o *Orchestrator) DockerStop( +// CertificateCreate creates a step that creates a CA certificate. +func (o *Orchestrator) CertificateCreate( target string, - id string, - opts osapi.DockerStopOpts, + opts osapi.CertificateCreateOpts, ) *Step { - name := o.nextOpName("docker-stop") + name := o.nextOpName("create-certificate") task := o.plan.TaskFunc( name, @@ -959,13 +3204,13 @@ func (o *Orchestrator) DockerStop( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Stop(ctx, target, id, opts) + resp, err := c.Certificate.Create(ctx, target, opts) if err != nil { - return nil, fmt.Errorf("docker stop: %w", err) + return nil, fmt.Errorf("create certificate: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerActionResult) engine.HostResult { + func(r osapi.CertificateCAMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -980,13 +3225,13 @@ func (o *Orchestrator) DockerStop( return &Step{task: task} } -// DockerRemove creates a step that removes a container from the target host. -func (o *Orchestrator) DockerRemove( +// CertificateUpdate creates a step that updates a CA certificate. +func (o *Orchestrator) CertificateUpdate( target string, - id string, - params *osapi.DockerRemoveParams, + certName string, + opts osapi.CertificateUpdateOpts, ) *Step { - name := o.nextOpName("docker-remove") + name := o.nextOpName("update-certificate") task := o.plan.TaskFunc( name, @@ -994,13 +3239,13 @@ func (o *Orchestrator) DockerRemove( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Remove(ctx, target, id, params) + resp, err := c.Certificate.Update(ctx, target, certName, opts) if err != nil { - return nil, fmt.Errorf("docker remove: %w", err) + return nil, fmt.Errorf("update certificate: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerActionResult) engine.HostResult { + func(r osapi.CertificateCAMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -1015,13 +3260,12 @@ func (o *Orchestrator) DockerRemove( return &Step{task: task} } -// DockerExec creates a step that executes a command inside a running container. -func (o *Orchestrator) DockerExec( +// CertificateDelete creates a step that deletes a CA certificate. +func (o *Orchestrator) CertificateDelete( target string, - id string, - opts osapi.DockerExecOpts, + certName string, ) *Step { - name := o.nextOpName("docker-exec") + name := o.nextOpName("delete-certificate") task := o.plan.TaskFunc( name, @@ -1029,13 +3273,13 @@ func (o *Orchestrator) DockerExec( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Exec(ctx, target, id, opts) + resp, err := c.Certificate.Delete(ctx, target, certName) if err != nil { - return nil, fmt.Errorf("docker exec: %w", err) + return nil, fmt.Errorf("delete certificate: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerExecResult) engine.HostResult { + func(r osapi.CertificateCAMutationResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -1050,12 +3294,15 @@ func (o *Orchestrator) DockerExec( return &Step{task: task} } -// DockerInspect creates a step that retrieves detailed info about a container. -func (o *Orchestrator) DockerInspect( +// --------------------------------------------------------------------------- +// Process +// --------------------------------------------------------------------------- + +// ProcessList creates a step that lists running processes. +func (o *Orchestrator) ProcessList( target string, - id string, ) *Step { - name := o.nextOpName("docker-inspect") + name := o.nextOpName("list-process") task := o.plan.TaskFunc( name, @@ -1063,17 +3310,16 @@ func (o *Orchestrator) DockerInspect( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.Inspect(ctx, target, id) + resp, err := c.Process.List(ctx, target) if err != nil { - return nil, fmt.Errorf("docker inspect: %w", err) + return nil, fmt.Errorf("list processes: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerDetailResult) engine.HostResult { + func(r osapi.ProcessInfoResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -1084,12 +3330,12 @@ func (o *Orchestrator) DockerInspect( return &Step{task: task} } -// DockerList creates a step that lists containers on the target host. -func (o *Orchestrator) DockerList( +// ProcessGet creates a step that retrieves a specific process by PID. +func (o *Orchestrator) ProcessGet( target string, - params *osapi.DockerListParams, + pid int, ) *Step { - name := o.nextOpName("docker-list") + name := o.nextOpName("get-process") task := o.plan.TaskFunc( name, @@ -1097,17 +3343,16 @@ func (o *Orchestrator) DockerList( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.List(ctx, target, params) + resp, err := c.Process.Get(ctx, target, pid) if err != nil { - return nil, fmt.Errorf("docker list: %w", err) + return nil, fmt.Errorf("get process: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerListResult) engine.HostResult { + func(r osapi.ProcessInfoResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -1118,11 +3363,13 @@ func (o *Orchestrator) DockerList( return &Step{task: task} } -// CronList creates a step that lists cron entries on the target host. -func (o *Orchestrator) CronList( +// ProcessSignal creates a step that sends a signal to a process. +func (o *Orchestrator) ProcessSignal( target string, + pid int, + opts osapi.ProcessSignalOpts, ) *Step { - name := o.nextOpName("list-cron") + name := o.nextOpName("signal-process") task := o.plan.TaskFunc( name, @@ -1130,17 +3377,17 @@ func (o *Orchestrator) CronList( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Schedule.CronList(ctx, target) + resp, err := c.Process.Signal(ctx, target, pid, opts) if err != nil { - return nil, fmt.Errorf("list cron: %w", err) + return nil, fmt.Errorf("signal process: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CronEntryResult) engine.HostResult { + func(r osapi.ProcessSignalResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: false, + Changed: r.Changed, Error: r.Error, } }, @@ -1151,12 +3398,16 @@ func (o *Orchestrator) CronList( return &Step{task: task} } -// CronGet creates a step that retrieves a specific cron entry on the target host. -func (o *Orchestrator) CronGet( +// --------------------------------------------------------------------------- +// Power +// --------------------------------------------------------------------------- + +// PowerReboot creates a step that initiates a reboot. +func (o *Orchestrator) PowerReboot( target string, - entryName string, + opts osapi.PowerOpts, ) *Step { - name := o.nextOpName("get-cron") + name := o.nextOpName("reboot") task := o.plan.TaskFunc( name, @@ -1164,17 +3415,17 @@ func (o *Orchestrator) CronGet( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Schedule.CronGet(ctx, target, entryName) + resp, err := c.Power.Reboot(ctx, target, opts) if err != nil { - return nil, fmt.Errorf("get cron: %w", err) + return nil, fmt.Errorf("reboot: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CronEntryResult) engine.HostResult { + func(r osapi.PowerResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: false, + Changed: r.Changed, Error: r.Error, } }, @@ -1185,12 +3436,12 @@ func (o *Orchestrator) CronGet( return &Step{task: task} } -// CronCreate creates a step that creates a new cron entry on the target host. -func (o *Orchestrator) CronCreate( +// PowerShutdown creates a step that initiates a shutdown. +func (o *Orchestrator) PowerShutdown( target string, - opts osapi.CronCreateOpts, + opts osapi.PowerOpts, ) *Step { - name := o.nextOpName("create-cron") + name := o.nextOpName("shutdown") task := o.plan.TaskFunc( name, @@ -1198,13 +3449,13 @@ func (o *Orchestrator) CronCreate( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Schedule.CronCreate(ctx, target, opts) + resp, err := c.Power.Shutdown(ctx, target, opts) if err != nil { - return nil, fmt.Errorf("create cron: %w", err) + return nil, fmt.Errorf("shutdown: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CronMutationResult) engine.HostResult { + func(r osapi.PowerResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, @@ -1219,13 +3470,16 @@ func (o *Orchestrator) CronCreate( return &Step{task: task} } -// CronUpdate creates a step that updates an existing cron entry on the target host. -func (o *Orchestrator) CronUpdate( +// --------------------------------------------------------------------------- +// Log +// --------------------------------------------------------------------------- + +// LogQuery creates a step that queries journal log entries. +func (o *Orchestrator) LogQuery( target string, - entryName string, - opts osapi.CronUpdateOpts, + opts osapi.LogQueryOpts, ) *Step { - name := o.nextOpName("update-cron") + name := o.nextOpName("query-log") task := o.plan.TaskFunc( name, @@ -1233,17 +3487,16 @@ func (o *Orchestrator) CronUpdate( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Schedule.CronUpdate(ctx, target, entryName, opts) + resp, err := c.Log.Query(ctx, target, opts) if err != nil { - return nil, fmt.Errorf("update cron: %w", err) + return nil, fmt.Errorf("query log: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CronMutationResult) engine.HostResult { + func(r osapi.LogEntryResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -1254,12 +3507,11 @@ func (o *Orchestrator) CronUpdate( return &Step{task: task} } -// CronDelete creates a step that deletes a cron entry on the target host. -func (o *Orchestrator) CronDelete( +// LogSources creates a step that lists available log sources. +func (o *Orchestrator) LogSources( target string, - entryName string, ) *Step { - name := o.nextOpName("delete-cron") + name := o.nextOpName("list-log-sources") task := o.plan.TaskFunc( name, @@ -1267,17 +3519,16 @@ func (o *Orchestrator) CronDelete( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Schedule.CronDelete(ctx, target, entryName) + resp, err := c.Log.Sources(ctx, target) if err != nil { - return nil, fmt.Errorf("delete cron: %w", err) + return nil, fmt.Errorf("list log sources: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.CronMutationResult) engine.HostResult { + func(r osapi.LogSourceResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, @@ -1288,14 +3539,13 @@ func (o *Orchestrator) CronDelete( return &Step{task: task} } -// DockerImageRemove creates a step that removes a container image -// from the target host. -func (o *Orchestrator) DockerImageRemove( +// LogQueryUnit creates a step that queries log entries for a systemd unit. +func (o *Orchestrator) LogQueryUnit( target string, - imageName string, - params *osapi.DockerImageRemoveParams, + unit string, + opts osapi.LogQueryOpts, ) *Step { - name := o.nextOpName("docker-image-remove") + name := o.nextOpName("query-log-unit") task := o.plan.TaskFunc( name, @@ -1303,22 +3553,16 @@ func (o *Orchestrator) DockerImageRemove( ctx context.Context, c *osapi.Client, ) (*engine.Result, error) { - resp, err := c.Docker.ImageRemove( - ctx, - target, - imageName, - params, - ) + resp, err := c.Log.QueryUnit(ctx, target, unit, opts) if err != nil { - return nil, fmt.Errorf("docker image remove: %w", err) + return nil, fmt.Errorf("query log unit: %w", err) } return engine.CollectionResult(resp.Data, resp.RawJSON(), - func(r osapi.DockerActionResult) engine.HostResult { + func(r osapi.LogEntryResult) engine.HostResult { return engine.HostResult{ Hostname: r.Hostname, Status: r.Status, - Changed: r.Changed, Error: r.Error, } }, From cafa2f285ad74a3a6e4223cf37c74de9cc917ee6 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: Mon, 6 Apr 2026 12:05:33 -0700 Subject: [PATCH 2/4] docs: add operation reference docs for all new domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 76 new documentation files across 13 new domain directories: sysctl, ntp, timezone, service, package, user, group, certificate, process, power, log, interface, route. Add dns-delete to existing network docs. Update operations README with full 101-operation count. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/operations/README.md | 39 ++++++++++----- docs/operations/certificate/README.md | 26 ++++++++++ docs/operations/certificate/create.md | 55 +++++++++++++++++++++ docs/operations/certificate/delete.md | 43 ++++++++++++++++ docs/operations/certificate/list.md | 50 +++++++++++++++++++ docs/operations/certificate/update.md | 53 ++++++++++++++++++++ docs/operations/group/README.md | 26 ++++++++++ docs/operations/group/create.md | 53 ++++++++++++++++++++ docs/operations/group/delete.md | 43 ++++++++++++++++ docs/operations/group/get.md | 43 ++++++++++++++++ docs/operations/group/list.md | 50 +++++++++++++++++++ docs/operations/group/update.md | 53 ++++++++++++++++++++ docs/operations/interface/README.md | 27 ++++++++++ docs/operations/interface/create.md | 59 ++++++++++++++++++++++ docs/operations/interface/delete.md | 43 ++++++++++++++++ docs/operations/interface/get.md | 43 ++++++++++++++++ docs/operations/interface/list.md | 42 ++++++++++++++++ docs/operations/interface/update.md | 48 ++++++++++++++++++ docs/operations/log/README.md | 23 +++++++++ docs/operations/log/query-unit.md | 47 ++++++++++++++++++ docs/operations/log/query.md | 65 +++++++++++++++++++++++++ docs/operations/log/sources.md | 42 ++++++++++++++++ docs/operations/network/README.md | 1 + docs/operations/network/dns-delete.md | 45 +++++++++++++++++ docs/operations/ntp/README.md | 25 ++++++++++ docs/operations/ntp/create.md | 52 ++++++++++++++++++++ docs/operations/ntp/delete.md | 43 ++++++++++++++++ docs/operations/ntp/get.md | 46 +++++++++++++++++ docs/operations/ntp/update.md | 51 +++++++++++++++++++ docs/operations/package/README.md | 28 +++++++++++ docs/operations/package/get.md | 43 ++++++++++++++++ docs/operations/package/install.md | 43 ++++++++++++++++ docs/operations/package/list-updates.md | 49 +++++++++++++++++++ docs/operations/package/list.md | 52 ++++++++++++++++++++ docs/operations/package/remove.md | 43 ++++++++++++++++ docs/operations/package/update.md | 41 ++++++++++++++++ docs/operations/power/README.md | 21 ++++++++ docs/operations/power/reboot.md | 54 ++++++++++++++++++++ docs/operations/power/shutdown.md | 54 ++++++++++++++++++++ docs/operations/process/README.md | 24 +++++++++ docs/operations/process/get.md | 43 ++++++++++++++++ docs/operations/process/list.md | 56 +++++++++++++++++++++ docs/operations/process/signal.md | 53 ++++++++++++++++++++ docs/operations/route/README.md | 26 ++++++++++ docs/operations/route/create.md | 62 +++++++++++++++++++++++ docs/operations/route/delete.md | 43 ++++++++++++++++ docs/operations/route/get.md | 43 ++++++++++++++++ docs/operations/route/list.md | 42 ++++++++++++++++ docs/operations/route/update.md | 49 +++++++++++++++++++ docs/operations/service/README.md | 32 ++++++++++++ docs/operations/service/create.md | 55 +++++++++++++++++++++ docs/operations/service/delete.md | 44 +++++++++++++++++ docs/operations/service/disable.md | 43 ++++++++++++++++ docs/operations/service/enable.md | 43 ++++++++++++++++ docs/operations/service/get.md | 43 ++++++++++++++++ docs/operations/service/list.md | 52 ++++++++++++++++++++ docs/operations/service/restart.md | 43 ++++++++++++++++ docs/operations/service/start.md | 43 ++++++++++++++++ docs/operations/service/stop.md | 43 ++++++++++++++++ docs/operations/service/update.md | 53 ++++++++++++++++++++ docs/operations/sysctl/README.md | 27 ++++++++++ docs/operations/sysctl/create.md | 54 ++++++++++++++++++++ docs/operations/sysctl/delete.md | 45 +++++++++++++++++ docs/operations/sysctl/get.md | 44 +++++++++++++++++ docs/operations/sysctl/list.md | 43 ++++++++++++++++ docs/operations/sysctl/update.md | 54 ++++++++++++++++++++ docs/operations/timezone/README.md | 24 +++++++++ docs/operations/timezone/get.md | 43 ++++++++++++++++ docs/operations/timezone/update.md | 52 ++++++++++++++++++++ docs/operations/user/README.md | 30 ++++++++++++ docs/operations/user/add-key.md | 51 +++++++++++++++++++ docs/operations/user/change-password.md | 44 +++++++++++++++++ docs/operations/user/create.md | 57 ++++++++++++++++++++++ docs/operations/user/delete.md | 43 ++++++++++++++++ docs/operations/user/get.md | 43 ++++++++++++++++ docs/operations/user/list-keys.md | 51 +++++++++++++++++++ docs/operations/user/list.md | 54 ++++++++++++++++++++ docs/operations/user/remove-key.md | 43 ++++++++++++++++ docs/operations/user/update.md | 57 ++++++++++++++++++++++ 79 files changed, 3445 insertions(+), 13 deletions(-) create mode 100644 docs/operations/certificate/README.md create mode 100644 docs/operations/certificate/create.md create mode 100644 docs/operations/certificate/delete.md create mode 100644 docs/operations/certificate/list.md create mode 100644 docs/operations/certificate/update.md create mode 100644 docs/operations/group/README.md create mode 100644 docs/operations/group/create.md create mode 100644 docs/operations/group/delete.md create mode 100644 docs/operations/group/get.md create mode 100644 docs/operations/group/list.md create mode 100644 docs/operations/group/update.md create mode 100644 docs/operations/interface/README.md create mode 100644 docs/operations/interface/create.md create mode 100644 docs/operations/interface/delete.md create mode 100644 docs/operations/interface/get.md create mode 100644 docs/operations/interface/list.md create mode 100644 docs/operations/interface/update.md create mode 100644 docs/operations/log/README.md create mode 100644 docs/operations/log/query-unit.md create mode 100644 docs/operations/log/query.md create mode 100644 docs/operations/log/sources.md create mode 100644 docs/operations/network/dns-delete.md create mode 100644 docs/operations/ntp/README.md create mode 100644 docs/operations/ntp/create.md create mode 100644 docs/operations/ntp/delete.md create mode 100644 docs/operations/ntp/get.md create mode 100644 docs/operations/ntp/update.md create mode 100644 docs/operations/package/README.md create mode 100644 docs/operations/package/get.md create mode 100644 docs/operations/package/install.md create mode 100644 docs/operations/package/list-updates.md create mode 100644 docs/operations/package/list.md create mode 100644 docs/operations/package/remove.md create mode 100644 docs/operations/package/update.md create mode 100644 docs/operations/power/README.md create mode 100644 docs/operations/power/reboot.md create mode 100644 docs/operations/power/shutdown.md create mode 100644 docs/operations/process/README.md create mode 100644 docs/operations/process/get.md create mode 100644 docs/operations/process/list.md create mode 100644 docs/operations/process/signal.md create mode 100644 docs/operations/route/README.md create mode 100644 docs/operations/route/create.md create mode 100644 docs/operations/route/delete.md create mode 100644 docs/operations/route/get.md create mode 100644 docs/operations/route/list.md create mode 100644 docs/operations/route/update.md create mode 100644 docs/operations/service/README.md create mode 100644 docs/operations/service/create.md create mode 100644 docs/operations/service/delete.md create mode 100644 docs/operations/service/disable.md create mode 100644 docs/operations/service/enable.md create mode 100644 docs/operations/service/get.md create mode 100644 docs/operations/service/list.md create mode 100644 docs/operations/service/restart.md create mode 100644 docs/operations/service/start.md create mode 100644 docs/operations/service/stop.md create mode 100644 docs/operations/service/update.md create mode 100644 docs/operations/sysctl/README.md create mode 100644 docs/operations/sysctl/create.md create mode 100644 docs/operations/sysctl/delete.md create mode 100644 docs/operations/sysctl/get.md create mode 100644 docs/operations/sysctl/list.md create mode 100644 docs/operations/sysctl/update.md create mode 100644 docs/operations/timezone/README.md create mode 100644 docs/operations/timezone/get.md create mode 100644 docs/operations/timezone/update.md create mode 100644 docs/operations/user/README.md create mode 100644 docs/operations/user/add-key.md create mode 100644 docs/operations/user/change-password.md create mode 100644 docs/operations/user/create.md create mode 100644 docs/operations/user/delete.md create mode 100644 docs/operations/user/get.md create mode 100644 docs/operations/user/list-keys.md create mode 100644 docs/operations/user/list.md create mode 100644 docs/operations/user/remove-key.md create mode 100644 docs/operations/user/update.md diff --git a/docs/operations/README.md b/docs/operations/README.md index 17e8338..9a0c90f 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -1,21 +1,34 @@ # Operations -The orchestrator provides 37 typed constructors organized by domain. Each method -returns a `*Step` that can be chained with ordering, conditions, and error -handling. +The orchestrator provides 101 typed constructors organized by domain. Each +method returns a `*Step` that can be chained with ordering, conditions, and +error handling. ## Domains -| Domain | Operations | Description | -| ------------------- | ---------- | ------------------------------------------------- | -| [Node](node/) | 8 | Hostname, disk, memory, load, uptime, OS, status | -| [Network](network/) | 3 | DNS configuration, ping | -| [Command](command/) | 2 | Direct exec, shell commands | -| [Docker](docker/) | 9 | Container lifecycle, exec, images | -| [Cron](cron/) | 5 | Cron drop-in file management | -| [File](file/) | 5 | Upload, deploy, status, undeploy, drift detection | -| [Agent](agent/) | 4 | Discovery, inspection, drain/undrain | -| [Health](health/) | 1 | Liveness check | +| Domain | Operations | Description | +| ----------------------------- | ---------- | ------------------------------------------------- | +| [Node](node/) | 8 | Hostname, disk, memory, load, uptime, OS, status | +| [Network](network/) | 4 | DNS configuration, ping | +| [Interface](interface/) | 5 | Network interface configuration via Netplan | +| [Route](route/) | 5 | Network route configuration via Netplan | +| [Command](command/) | 2 | Direct exec, shell commands | +| [Docker](docker/) | 9 | Container lifecycle, exec, images | +| [Cron](cron/) | 5 | Cron drop-in file management | +| [Sysctl](sysctl/) | 5 | Kernel parameter management | +| [NTP](ntp/) | 4 | NTP server configuration | +| [Timezone](timezone/) | 2 | System timezone management | +| [Service](service/) | 10 | Systemd service lifecycle and unit files | +| [Package](package/) | 6 | System package management | +| [User](user/) | 9 | User accounts, SSH keys, passwords | +| [Group](group/) | 5 | Local group management | +| [Certificate](certificate/) | 4 | CA certificate trust store management | +| [Process](process/) | 3 | Process listing, inspection, signals | +| [Power](power/) | 2 | Reboot and shutdown | +| [Log](log/) | 3 | Systemd journal queries | +| [File](file/) | 5 | Upload, deploy, status, undeploy, drift detection | +| [Agent](agent/) | 4 | Discovery, inspection, drain/undrain | +| [Health](health/) | 1 | Liveness check | ## Idempotency diff --git a/docs/operations/certificate/README.md b/docs/operations/certificate/README.md new file mode 100644 index 0000000..ec40d54 --- /dev/null +++ b/docs/operations/certificate/README.md @@ -0,0 +1,26 @@ +# Certificate Management + +Manage CA certificates in the system trust store on target nodes -- list, +create, update, and delete certificates backed by the file provider. + +## Operations + +| Method | Description | Idempotent | +| ------------------------------------------------------------- | ------------------------- | -------------- | +| [`CertificateList(target)`](list.md) | List CA certificates | Read-only | +| [`CertificateCreate(target, opts)`](create.md) | Create a CA certificate | Non-idempotent | +| [`CertificateUpdate(target, name, opts)`](update.md) | Update a CA certificate | Idempotent | +| [`CertificateDelete(target, name)`](delete.md) | Delete a CA certificate | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | ------------------- | +| Read operations | `certificate:read` | +| Write operations | `certificate:write` | + +## Example + +See +[`examples/operations/certificate.go`](../../examples/operations/certificate.go) +for a complete workflow example covering all operations. diff --git a/docs/operations/certificate/create.md b/docs/operations/certificate/create.md new file mode 100644 index 0000000..101e1ce --- /dev/null +++ b/docs/operations/certificate/create.md @@ -0,0 +1,55 @@ +# CertificateCreate + +Creates a CA certificate in the system trust store. The PEM file must be +uploaded to the NATS Object Store first (see [FileUpload](../file/upload.md)). + +## Usage + +```go +step := o.CertificateCreate("web-01", osapi.CertificateCreateOpts{ + Name: "internal-ca", + Object: "internal-ca.pem", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ----------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `CertificateCreateOpts` | Create options (see below). | + +### CertificateCreateOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ---------------------------------------------- | +| `Name` | `string` | Yes | Certificate name. | +| `Object` | `string` | Yes | Object Store reference for the PEM file. | + +## Result Type + +```go +var result osapi.CertificateCAMutationResult +err := results.Decode("create-certificate-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Certificate name. | +| `Changed` | `bool` | Whether the certificate was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating a certificate that already exists returns an +error. Use [CertificateUpdate](update.md) to replace existing certificates. + +## Permissions + +Requires `certificate:write` permission. + +## Example + +See +[`examples/operations/certificate.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/certificate.go) +for a complete working example. diff --git a/docs/operations/certificate/delete.md b/docs/operations/certificate/delete.md new file mode 100644 index 0000000..cd5e04b --- /dev/null +++ b/docs/operations/certificate/delete.md @@ -0,0 +1,43 @@ +# CertificateDelete + +Deletes a CA certificate from the system trust store. + +## Usage + +```go +step := o.CertificateDelete("web-01", "internal-ca") +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `certName` | `string` | Name of the certificate to delete. | + +## Result Type + +```go +var result osapi.CertificateCAMutationResult +err := results.Decode("delete-certificate-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| `Name` | `string` | Certificate name. | +| `Changed` | `bool` | Whether the certificate was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the certificate does not exist, returns `Changed: false`. + +## Permissions + +Requires `certificate:write` permission. + +## Example + +See +[`examples/operations/certificate.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/certificate.go) +for a complete working example. diff --git a/docs/operations/certificate/list.md b/docs/operations/certificate/list.md new file mode 100644 index 0000000..a9bf551 --- /dev/null +++ b/docs/operations/certificate/list.md @@ -0,0 +1,50 @@ +# CertificateList + +Lists CA certificates in the system trust store on the target node. + +## Usage + +```go +step := o.CertificateList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.CertificateCAResult +err := results.Decode("list-certificate-1", &result) +``` + +| Field | Type | Description | +| -------------- | ----------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Certificates` | `[]CertificateCA` | List of CA certificate entries. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### CertificateCA + +| Field | Type | Description | +| -------- | -------- | --------------------------------------- | +| `Name` | `string` | Certificate name. | +| `Source` | `string` | Path to the certificate on the filesystem. | +| `Object` | `string` | Object Store reference. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `certificate:read` permission. + +## Example + +See +[`examples/operations/certificate.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/certificate.go) +for a complete working example. diff --git a/docs/operations/certificate/update.md b/docs/operations/certificate/update.md new file mode 100644 index 0000000..823036d --- /dev/null +++ b/docs/operations/certificate/update.md @@ -0,0 +1,53 @@ +# CertificateUpdate + +Updates a CA certificate in the system trust store. + +## Usage + +```go +step := o.CertificateUpdate("web-01", "internal-ca", osapi.CertificateUpdateOpts{ + Object: "internal-ca-v2.pem", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | ----------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `certName` | `string` | Name of the certificate to update. | +| `opts` | `CertificateUpdateOpts` | Update options (see below). | + +### CertificateUpdateOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ---------------------------------------------- | +| `Object` | `string` | Yes | New Object Store reference for the PEM file. | + +## Result Type + +```go +var result osapi.CertificateCAMutationResult +err := results.Decode("update-certificate-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Certificate name. | +| `Changed` | `bool` | Whether the certificate was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current PEM content against the new content. +Returns `Changed: true` only if the certificate was actually modified. + +## Permissions + +Requires `certificate:write` permission. + +## Example + +See +[`examples/operations/certificate.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/certificate.go) +for a complete working example. diff --git a/docs/operations/group/README.md b/docs/operations/group/README.md new file mode 100644 index 0000000..51078fc --- /dev/null +++ b/docs/operations/group/README.md @@ -0,0 +1,26 @@ +# Group Management + +Manage local groups on target nodes -- list, get, create, update, and delete +groups. + +## Operations + +| Method | Description | Idempotent | +| --------------------------------------------------------- | ---------------------- | -------------- | +| [`GroupList(target)`](list.md) | List groups | Read-only | +| [`GroupGet(target, name)`](get.md) | Get a specific group | Read-only | +| [`GroupCreate(target, opts)`](create.md) | Create a group | Non-idempotent | +| [`GroupUpdate(target, name, opts)`](update.md) | Update a group | Idempotent | +| [`GroupDelete(target, name)`](delete.md) | Delete a group | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | ------------ | +| Read operations | `user:read` | +| Write operations | `user:write` | + +## Example + +See [`examples/operations/user.go`](../../examples/operations/user.go) for a +complete workflow example covering group operations. diff --git a/docs/operations/group/create.md b/docs/operations/group/create.md new file mode 100644 index 0000000..9f3ec45 --- /dev/null +++ b/docs/operations/group/create.md @@ -0,0 +1,53 @@ +# GroupCreate + +Creates a group on the target node. + +## Usage + +```go +step := o.GroupCreate("web-01", osapi.GroupCreateOpts{ + Name: "developers", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ----------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `GroupCreateOpts` | Create options (see below). | + +### GroupCreateOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | -------------------------------------------- | +| `Name` | `string` | Yes | Group name. | +| `GID` | `int` | No | Numeric group ID (system assigns if zero). | +| `System` | `bool` | No | Create a system group. | + +## Result Type + +```go +var result osapi.GroupMutationResult +err := results.Decode("create-group-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Group name. | +| `Changed` | `bool` | Whether the group was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating a group that already exists returns an error. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/group/delete.md b/docs/operations/group/delete.md new file mode 100644 index 0000000..e1b9c5d --- /dev/null +++ b/docs/operations/group/delete.md @@ -0,0 +1,43 @@ +# GroupDelete + +Deletes a group from the target node. + +## Usage + +```go +step := o.GroupDelete("web-01", "developers") +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `groupName` | `string` | Name of the group to delete. | + +## Result Type + +```go +var result osapi.GroupMutationResult +err := results.Decode("delete-group-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Name` | `string` | Group name. | +| `Changed` | `bool` | Whether the group was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the group does not exist, returns `Changed: false`. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/group/get.md b/docs/operations/group/get.md new file mode 100644 index 0000000..1874530 --- /dev/null +++ b/docs/operations/group/get.md @@ -0,0 +1,43 @@ +# GroupGet + +Retrieves a specific group by name from the target node. + +## Usage + +```go +step := o.GroupGet("web-01", "developers") +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `groupName` | `string` | Name of the group to retrieve. | + +## Result Type + +```go +var result osapi.GroupInfoResult +err := results.Decode("get-group-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Groups` | `[]GroupInfo` | Group details (single entry). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `user:read` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/group/list.md b/docs/operations/group/list.md new file mode 100644 index 0000000..d854775 --- /dev/null +++ b/docs/operations/group/list.md @@ -0,0 +1,50 @@ +# GroupList + +Lists groups on the target node. + +## Usage + +```go +step := o.GroupList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.GroupInfoResult +err := results.Decode("list-group-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Groups` | `[]GroupInfo` | List of groups. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### GroupInfo + +| Field | Type | Description | +| --------- | ---------- | --------------------- | +| `Name` | `string` | Group name. | +| `GID` | `int` | Numeric group ID. | +| `Members` | `[]string` | Group member names. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `user:read` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/group/update.md b/docs/operations/group/update.md new file mode 100644 index 0000000..5e6fa25 --- /dev/null +++ b/docs/operations/group/update.md @@ -0,0 +1,53 @@ +# GroupUpdate + +Updates a group on the target node. + +## Usage + +```go +step := o.GroupUpdate("web-01", "developers", osapi.GroupUpdateOpts{ + Members: []string{"alice", "bob"}, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | ----------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `groupName` | `string` | Name of the group to update. | +| `opts` | `GroupUpdateOpts` | Update options (see below). | + +### GroupUpdateOpts + +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ---------------------------------------------- | +| `Members` | `[]string` | No | Group member usernames (replaces existing). | + +## Result Type + +```go +var result osapi.GroupMutationResult +err := results.Decode("update-group-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Group name. | +| `Changed` | `bool` | Whether the group was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current members against the desired list. Returns +`Changed: true` only if the group was actually modified. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/interface/README.md b/docs/operations/interface/README.md new file mode 100644 index 0000000..312f764 --- /dev/null +++ b/docs/operations/interface/README.md @@ -0,0 +1,27 @@ +# Interface Management + +Manage network interface configuration via Netplan on target nodes -- list, get, +create, update, and delete interface configurations. + +## Operations + +| Method | Description | Idempotent | +| ------------------------------------------------------------- | ----------------------------------- | -------------- | +| [`InterfaceList(target)`](list.md) | List network interfaces | Read-only | +| [`InterfaceGet(target, name)`](get.md) | Get a specific interface | Read-only | +| [`InterfaceCreate(target, name, opts)`](create.md) | Create an interface configuration | Non-idempotent | +| [`InterfaceUpdate(target, name, opts)`](update.md) | Update an interface configuration | Idempotent | +| [`InterfaceDelete(target, name)`](delete.md) | Delete an interface configuration | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | --------------- | +| Read operations | `network:read` | +| Write operations | `network:write` | + +## Example + +See +[`examples/operations/interface.go`](../../examples/operations/interface.go) for +a complete workflow example covering all operations. diff --git a/docs/operations/interface/create.md b/docs/operations/interface/create.md new file mode 100644 index 0000000..13e96a3 --- /dev/null +++ b/docs/operations/interface/create.md @@ -0,0 +1,59 @@ +# InterfaceCreate + +Creates a network interface configuration on the target node via Netplan. + +## Usage + +```go +dhcp4 := true +step := o.InterfaceCreate("web-01", "eth1", osapi.InterfaceConfigOpts{ + DHCP4: &dhcp4, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | --------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `ifaceName` | `string` | Name of the network interface to configure. | +| `opts` | `InterfaceConfigOpts` | Configuration options (see below). | + +### InterfaceConfigOpts + +| Field | Type | Required | Description | +| ----------- | ---------- | -------- | ---------------------------------------- | +| `DHCP4` | `*bool` | No | Enable or disable DHCPv4. | +| `DHCP6` | `*bool` | No | Enable or disable DHCPv6. | +| `Addresses` | `[]string` | No | IP addresses in CIDR notation. | +| `Gateway4` | `string` | No | IPv4 gateway address. | +| `Gateway6` | `string` | No | IPv6 gateway address. | +| `MTU` | `*int` | No | Maximum transmission unit. | + +## Result Type + +```go +var result osapi.InterfaceMutationResult +err := results.Decode("create-interface-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Interface name. | +| `Changed` | `bool` | Whether the configuration was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating an interface configuration that already exists +returns an error. Use [InterfaceUpdate](update.md) to modify existing configs. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/interface.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/interface.go) +for a complete working example. diff --git a/docs/operations/interface/delete.md b/docs/operations/interface/delete.md new file mode 100644 index 0000000..d54d6a7 --- /dev/null +++ b/docs/operations/interface/delete.md @@ -0,0 +1,43 @@ +# InterfaceDelete + +Deletes a network interface configuration from the target node. + +## Usage + +```go +step := o.InterfaceDelete("web-01", "eth1") +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `ifaceName` | `string` | Name of the network interface to delete. | + +## Result Type + +```go +var result osapi.InterfaceMutationResult +err := results.Decode("delete-interface-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------------- | +| `Name` | `string` | Interface name. | +| `Changed` | `bool` | Whether the configuration was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the configuration does not exist, returns `Changed: false`. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/interface.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/interface.go) +for a complete working example. diff --git a/docs/operations/interface/get.md b/docs/operations/interface/get.md new file mode 100644 index 0000000..c1fc4f2 --- /dev/null +++ b/docs/operations/interface/get.md @@ -0,0 +1,43 @@ +# InterfaceGet + +Retrieves a specific network interface configuration from the target node. + +## Usage + +```go +step := o.InterfaceGet("web-01", "eth0") +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `ifaceName` | `string` | Name of the network interface. | + +## Result Type + +```go +var result osapi.InterfaceGetResult +err := results.Decode("get-interface-1", &result) +``` + +| Field | Type | Description | +| ----------- | ---------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Interface` | `*InterfaceInfo` | Interface details. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `network:read` permission. + +## Example + +See +[`examples/operations/interface.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/interface.go) +for a complete working example. diff --git a/docs/operations/interface/list.md b/docs/operations/interface/list.md new file mode 100644 index 0000000..8dec606 --- /dev/null +++ b/docs/operations/interface/list.md @@ -0,0 +1,42 @@ +# InterfaceList + +Lists network interfaces on the target node. + +## Usage + +```go +step := o.InterfaceList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.InterfaceListResult +err := results.Decode("list-interface-1", &result) +``` + +| Field | Type | Description | +| ------------ | ----------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Interfaces` | `[]InterfaceInfo` | List of network interfaces. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `network:read` permission. + +## Example + +See +[`examples/operations/interface.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/interface.go) +for a complete working example. diff --git a/docs/operations/interface/update.md b/docs/operations/interface/update.md new file mode 100644 index 0000000..8bb1815 --- /dev/null +++ b/docs/operations/interface/update.md @@ -0,0 +1,48 @@ +# InterfaceUpdate + +Updates a network interface configuration on the target node. + +## Usage + +```go +mtu := 9000 +step := o.InterfaceUpdate("web-01", "eth0", osapi.InterfaceConfigOpts{ + MTU: &mtu, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ----------- | --------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `ifaceName` | `string` | Name of the network interface to update. | +| `opts` | `InterfaceConfigOpts` | Configuration options (see [InterfaceCreate](create.md)). | + +## Result Type + +```go +var result osapi.InterfaceMutationResult +err := results.Decode("update-interface-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Interface name. | +| `Changed` | `bool` | Whether the configuration was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current configuration against the desired state. +Returns `Changed: true` only if the configuration was actually modified. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/interface.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/interface.go) +for a complete working example. diff --git a/docs/operations/log/README.md b/docs/operations/log/README.md new file mode 100644 index 0000000..93232c1 --- /dev/null +++ b/docs/operations/log/README.md @@ -0,0 +1,23 @@ +# Log Management + +Query systemd journal logs on target nodes -- query all entries, list log +sources, and query by unit name. + +## Operations + +| Method | Description | Idempotent | +| ---------------------------------------------------- | -------------------------- | ---------- | +| [`LogQuery(target, opts)`](query.md) | Query journal log entries | Read-only | +| [`LogSources(target)`](sources.md) | List available log sources | Read-only | +| [`LogQueryUnit(target, unit, opts)`](query-unit.md) | Query logs for a unit | Read-only | + +## Permissions + +| Operation | Permission | +| --------------- | ---------- | +| All operations | `log:read` | + +## Example + +See [`examples/operations/log.go`](../../examples/operations/log.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/log/query-unit.md b/docs/operations/log/query-unit.md new file mode 100644 index 0000000..86d9113 --- /dev/null +++ b/docs/operations/log/query-unit.md @@ -0,0 +1,47 @@ +# LogQueryUnit + +Queries journal log entries for a specific systemd unit on the target node. + +## Usage + +```go +lines := 100 +step := o.LogQueryUnit("web-01", "nginx", osapi.LogQueryOpts{ + Lines: &lines, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `unit` | `string` | Systemd unit name to query logs for. | +| `opts` | `LogQueryOpts` | Query options (see [LogQuery](query.md)). | + +## Result Type + +```go +var result osapi.LogEntryResult +err := results.Decode("query-log-unit-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------ | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Entries` | `[]LogEntry` | List of journal entries for the unit. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `log:read` permission. + +## Example + +See +[`examples/operations/log.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/log.go) +for a complete working example. diff --git a/docs/operations/log/query.md b/docs/operations/log/query.md new file mode 100644 index 0000000..206f41c --- /dev/null +++ b/docs/operations/log/query.md @@ -0,0 +1,65 @@ +# LogQuery + +Queries journal log entries on the target node. + +## Usage + +```go +lines := 50 +step := o.LogQuery("web-01", osapi.LogQueryOpts{ + Lines: &lines, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `LogQueryOpts` | Query options (see below). | + +### LogQueryOpts + +| Field | Type | Required | Description | +| ---------- | --------- | -------- | ------------------------------------------------------ | +| `Lines` | `*int` | No | Maximum number of log lines to return. | +| `Since` | `*string` | No | Filter entries since this time (e.g., `1h`). | +| `Priority` | `*string` | No | Filter by priority level (e.g., `err`, `warning`). | + +## Result Type + +```go +var result osapi.LogEntryResult +err := results.Decode("query-log-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------ | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Entries` | `[]LogEntry` | List of journal entries. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### LogEntry + +| Field | Type | Description | +| ----------- | -------- | ------------------------ | +| `Timestamp` | `string` | Entry timestamp. | +| `Unit` | `string` | Systemd unit name. | +| `Priority` | `string` | Log priority level. | +| `Message` | `string` | Log message. | +| `PID` | `int` | Process ID. | +| `Hostname` | `string` | Source hostname. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `log:read` permission. + +## Example + +See +[`examples/operations/log.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/log.go) +for a complete working example. diff --git a/docs/operations/log/sources.md b/docs/operations/log/sources.md new file mode 100644 index 0000000..a49fc97 --- /dev/null +++ b/docs/operations/log/sources.md @@ -0,0 +1,42 @@ +# LogSources + +Lists available log sources (systemd units) on the target node. + +## Usage + +```go +step := o.LogSources("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.LogSourceResult +err := results.Decode("list-log-sources-1", &result) +``` + +| Field | Type | Description | +| ---------- | ---------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Sources` | `[]string` | List of available log source names. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `log:read` permission. + +## Example + +See +[`examples/operations/log.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/log.go) +for a complete working example. diff --git a/docs/operations/network/README.md b/docs/operations/network/README.md index 9c5c5a4..9175c58 100644 --- a/docs/operations/network/README.md +++ b/docs/operations/network/README.md @@ -9,6 +9,7 @@ connectivity checks. | -------------------------------------------------------- | -------------------------------------- | ---------- | | [`NetworkDNSGet(target, iface)`](dns-get.md) | Get DNS configuration for an interface | Read-only | | [`NetworkDNSUpdate(target, iface, opts)`](dns-update.md) | Update DNS servers for an interface | Idempotent | +| [`NetworkDNSDelete(target, iface)`](dns-delete.md) | Delete DNS configuration | Idempotent | | [`NetworkPingDo(target, address)`](ping.md) | Ping a host from the target node | Read-only | ## Permissions diff --git a/docs/operations/network/dns-delete.md b/docs/operations/network/dns-delete.md new file mode 100644 index 0000000..f9b5a8f --- /dev/null +++ b/docs/operations/network/dns-delete.md @@ -0,0 +1,45 @@ +# NetworkDNSDelete + +Deletes the DNS configuration for a specific network interface on the target +node. + +## Usage + +```go +step := o.NetworkDNSDelete("web-01", "eth0") +``` + +## Parameters + +| Parameter | Type | Description | +| --------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `interfaceName` | `string` | Network interface to delete DNS configuration from. | + +## Result Type + +```go +var result osapi.DNSDeleteResult +err := results.Decode("delete-dns-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Changed` | `bool` | Whether the DNS config was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If no DNS configuration exists for the interface, returns +`Changed: false`. If configuration exists and is removed, returns +`Changed: true`. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/dns-update.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/dns-update.go) +for a complete working example. diff --git a/docs/operations/ntp/README.md b/docs/operations/ntp/README.md new file mode 100644 index 0000000..9a6b574 --- /dev/null +++ b/docs/operations/ntp/README.md @@ -0,0 +1,25 @@ +# NTP Management + +Manage NTP server configuration on target nodes -- get status, create, update, +and delete NTP configuration. + +## Operations + +| Method | Description | Idempotent | +| ----------------------------------------- | ------------------------ | -------------- | +| [`NTPGet(target)`](get.md) | Get NTP status | Read-only | +| [`NTPCreate(target, opts)`](create.md) | Create NTP configuration | Non-idempotent | +| [`NTPUpdate(target, opts)`](update.md) | Update NTP configuration | Idempotent | +| [`NTPDelete(target)`](delete.md) | Delete NTP configuration | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | ----------- | +| Read operations | `ntp:read` | +| Write operations | `ntp:write` | + +## Example + +See [`examples/operations/ntp.go`](../../examples/operations/ntp.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/ntp/create.md b/docs/operations/ntp/create.md new file mode 100644 index 0000000..94a8303 --- /dev/null +++ b/docs/operations/ntp/create.md @@ -0,0 +1,52 @@ +# NTPCreate + +Creates NTP configuration on the target node by deploying a timesyncd +configuration file with the specified servers. + +## Usage + +```go +step := o.NTPCreate("web-01", osapi.NtpCreateOpts{ + Servers: []string{"0.pool.ntp.org", "1.pool.ntp.org"}, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | --------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `NtpCreateOpts` | Create options (see below). | + +### NtpCreateOpts + +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ----------------------------------- | +| `Servers` | `[]string` | Yes | NTP server addresses to configure. | + +## Result Type + +```go +var result osapi.NtpMutationResult +err := results.Decode("create-ntp-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Changed` | `bool` | Whether the configuration was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating configuration that already exists returns an +error. Use [NTPUpdate](update.md) to modify existing configuration. + +## Permissions + +Requires `ntp:write` permission. + +## Example + +See +[`examples/operations/ntp.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/ntp.go) +for a complete working example. diff --git a/docs/operations/ntp/delete.md b/docs/operations/ntp/delete.md new file mode 100644 index 0000000..658eec8 --- /dev/null +++ b/docs/operations/ntp/delete.md @@ -0,0 +1,43 @@ +# NTPDelete + +Deletes the NTP configuration from the target node. + +## Usage + +```go +step := o.NTPDelete("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.NtpMutationResult +err := results.Decode("delete-ntp-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ----------------------------------------------------- | +| `Changed` | `bool` | Whether the configuration was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If no NTP configuration exists, the operation returns +`Changed: false`. If configuration exists and is removed, returns +`Changed: true`. + +## Permissions + +Requires `ntp:write` permission. + +## Example + +See +[`examples/operations/ntp.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/ntp.go) +for a complete working example. diff --git a/docs/operations/ntp/get.md b/docs/operations/ntp/get.md new file mode 100644 index 0000000..bb078d7 --- /dev/null +++ b/docs/operations/ntp/get.md @@ -0,0 +1,46 @@ +# NTPGet + +Retrieves the NTP status and configuration from the target node. + +## Usage + +```go +step := o.NTPGet("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.NtpStatusResult +err := results.Decode("get-ntp-1", &result) +``` + +| Field | Type | Description | +| --------------- | ---------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Synchronized` | `bool` | Whether NTP is synchronized. | +| `Stratum` | `int` | NTP stratum level. | +| `Offset` | `string` | Clock offset from the NTP source. | +| `CurrentSource` | `string` | The currently selected NTP source. | +| `Servers` | `[]string` | Configured NTP server addresses. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `ntp:read` permission. + +## Example + +See +[`examples/operations/ntp.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/ntp.go) +for a complete working example. diff --git a/docs/operations/ntp/update.md b/docs/operations/ntp/update.md new file mode 100644 index 0000000..9553c66 --- /dev/null +++ b/docs/operations/ntp/update.md @@ -0,0 +1,51 @@ +# NTPUpdate + +Updates the NTP configuration on the target node. + +## Usage + +```go +step := o.NTPUpdate("web-01", osapi.NtpUpdateOpts{ + Servers: []string{"time.google.com", "time.cloudflare.com"}, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | --------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `NtpUpdateOpts` | Update options (see below). | + +### NtpUpdateOpts + +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ----------------------------------- | +| `Servers` | `[]string` | Yes | NTP server addresses to configure. | + +## Result Type + +```go +var result osapi.NtpMutationResult +err := results.Decode("update-ntp-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Changed` | `bool` | Whether the configuration was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current NTP servers against the desired list. +Returns `Changed: true` only if the configuration was actually modified. + +## Permissions + +Requires `ntp:write` permission. + +## Example + +See +[`examples/operations/ntp.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/ntp.go) +for a complete working example. diff --git a/docs/operations/package/README.md b/docs/operations/package/README.md new file mode 100644 index 0000000..b1d9425 --- /dev/null +++ b/docs/operations/package/README.md @@ -0,0 +1,28 @@ +# Package Management + +Manage system packages on target nodes -- list installed packages, get details, +install, remove, update all, and check for available updates. + +## Operations + +| Method | Description | Idempotent | +| --------------------------------------------------- | ------------------------------ | -------------- | +| [`PackageList(target)`](list.md) | List installed packages | Read-only | +| [`PackageGet(target, name)`](get.md) | Get a specific package | Read-only | +| [`PackageInstall(target, name)`](install.md) | Install a package | Idempotent | +| [`PackageRemove(target, name)`](remove.md) | Remove a package | Idempotent | +| [`PackageUpdate(target)`](update.md) | Update all packages | Non-idempotent | +| [`PackageListUpdates(target)`](list-updates.md) | List available package updates | Read-only | + +## Permissions + +| Operation | Permission | +| ---------------- | --------------- | +| Read operations | `package:read` | +| Write operations | `package:write` | + +## Example + +See +[`examples/operations/package.go`](../../examples/operations/package.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/package/get.md b/docs/operations/package/get.md new file mode 100644 index 0000000..5daa69b --- /dev/null +++ b/docs/operations/package/get.md @@ -0,0 +1,43 @@ +# PackageGet + +Retrieves information about a specific installed package. + +## Usage + +```go +step := o.PackageGet("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `pkgName` | `string` | Name of the package to retrieve. | + +## Result Type + +```go +var result osapi.PackageInfoResult +err := results.Decode("get-package-1", &result) +``` + +| Field | Type | Description | +| ---------- | --------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Packages` | `[]PackageInfo` | Package details (single entry). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `package:read` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/package/install.md b/docs/operations/package/install.md new file mode 100644 index 0000000..f703522 --- /dev/null +++ b/docs/operations/package/install.md @@ -0,0 +1,43 @@ +# PackageInstall + +Installs a package on the target node. + +## Usage + +```go +step := o.PackageInstall("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `pkgName` | `string` | Name of the package to install. | + +## Result Type + +```go +var result osapi.PackageMutationResult +err := results.Decode("install-package-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Name` | `string` | Name of the package. | +| `Changed` | `bool` | Whether the package was installed. | +| `Error` | `string` | Error message if install failed; empty on success. | + +## Idempotency + +**Idempotent.** If the package is already installed, returns `Changed: false`. + +## Permissions + +Requires `package:write` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/package/list-updates.md b/docs/operations/package/list-updates.md new file mode 100644 index 0000000..9a870fc --- /dev/null +++ b/docs/operations/package/list-updates.md @@ -0,0 +1,49 @@ +# PackageListUpdates + +Lists available package updates on the target node. + +## Usage + +```go +step := o.PackageListUpdates("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.PackageUpdateResult +err := results.Decode("list-package-updates-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Updates` | `[]UpdateInfo` | List of available updates. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### UpdateInfo + +| Field | Type | Description | +| ---------------- | -------- | ------------------------------ | +| `Name` | `string` | Package name. | +| `CurrentVersion` | `string` | Currently installed version. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `package:read` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/package/list.md b/docs/operations/package/list.md new file mode 100644 index 0000000..4f00f9a --- /dev/null +++ b/docs/operations/package/list.md @@ -0,0 +1,52 @@ +# PackageList + +Lists installed packages on the target node. + +## Usage + +```go +step := o.PackageList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.PackageInfoResult +err := results.Decode("list-package-1", &result) +``` + +| Field | Type | Description | +| ---------- | --------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Packages` | `[]PackageInfo` | List of installed packages. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### PackageInfo + +| Field | Type | Description | +| ------------- | -------- | ------------------------- | +| `Name` | `string` | Package name. | +| `Version` | `string` | Installed version. | +| `Description` | `string` | Package description. | +| `Status` | `string` | Installation status. | +| `Size` | `int64` | Installed size in bytes. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `package:read` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/package/remove.md b/docs/operations/package/remove.md new file mode 100644 index 0000000..f338b29 --- /dev/null +++ b/docs/operations/package/remove.md @@ -0,0 +1,43 @@ +# PackageRemove + +Removes a package from the target node. + +## Usage + +```go +step := o.PackageRemove("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `pkgName` | `string` | Name of the package to remove. | + +## Result Type + +```go +var result osapi.PackageMutationResult +err := results.Decode("remove-package-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Name of the package. | +| `Changed` | `bool` | Whether the package was removed or already absent. | +| `Error` | `string` | Error message if removal failed; empty on success. | + +## Idempotency + +**Idempotent.** If the package is not installed, returns `Changed: false`. + +## Permissions + +Requires `package:write` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/package/update.md b/docs/operations/package/update.md new file mode 100644 index 0000000..cf3dd5e --- /dev/null +++ b/docs/operations/package/update.md @@ -0,0 +1,41 @@ +# PackageUpdate + +Updates all packages on the target node. + +## Usage + +```go +step := o.PackageUpdate("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.PackageMutationResult +err := results.Decode("update-package-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Changed` | `bool` | Whether any packages were updated. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always runs the package update process. + +## Permissions + +Requires `package:write` permission. + +## Example + +See +[`examples/operations/package.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/package.go) +for a complete working example. diff --git a/docs/operations/power/README.md b/docs/operations/power/README.md new file mode 100644 index 0000000..3fab5cc --- /dev/null +++ b/docs/operations/power/README.md @@ -0,0 +1,21 @@ +# Power Management + +Reboot or shut down target nodes. + +## Operations + +| Method | Description | Idempotent | +| -------------------------------------------- | ----------------------- | -------------- | +| [`PowerReboot(target, opts)`](reboot.md) | Reboot the node | Non-idempotent | +| [`PowerShutdown(target, opts)`](shutdown.md) | Shut down the node | Non-idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | --------------- | +| All operations | `power:execute` | + +## Example + +See [`examples/operations/power.go`](../../examples/operations/power.go) for a +complete workflow example. diff --git a/docs/operations/power/reboot.md b/docs/operations/power/reboot.md new file mode 100644 index 0000000..85ef111 --- /dev/null +++ b/docs/operations/power/reboot.md @@ -0,0 +1,54 @@ +# PowerReboot + +Initiates a reboot on the target node. + +## Usage + +```go +step := o.PowerReboot("web-01", osapi.PowerOpts{ + Delay: 30, + Message: "Scheduled maintenance reboot", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ----------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `PowerOpts` | Power options (see below). | + +### PowerOpts + +| Field | Type | Required | Description | +| --------- | -------- | -------- | ------------------------------------------------- | +| `Delay` | `int` | No | Seconds to wait before rebooting. | +| `Message` | `string` | No | Message to broadcast before the reboot. | + +## Result Type + +```go +var result osapi.PowerResult +err := results.Decode("reboot-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Action` | `string` | Action taken (e.g., `reboot`). | +| `Delay` | `int` | Delay in seconds before execution. | +| `Changed` | `bool` | Whether the reboot was initiated. | +| `Error` | `string` | Error message if reboot failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always initiates a reboot. + +## Permissions + +Requires `power:execute` permission. + +## Example + +See +[`examples/operations/power.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/power.go) +for a complete working example. diff --git a/docs/operations/power/shutdown.md b/docs/operations/power/shutdown.md new file mode 100644 index 0000000..bdcd20a --- /dev/null +++ b/docs/operations/power/shutdown.md @@ -0,0 +1,54 @@ +# PowerShutdown + +Initiates a shutdown on the target node. + +## Usage + +```go +step := o.PowerShutdown("web-01", osapi.PowerOpts{ + Delay: 60, + Message: "System decommission", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ----------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `PowerOpts` | Power options (see below). | + +### PowerOpts + +| Field | Type | Required | Description | +| --------- | -------- | -------- | ------------------------------------------------- | +| `Delay` | `int` | No | Seconds to wait before shutting down. | +| `Message` | `string` | No | Message to broadcast before the shutdown. | + +## Result Type + +```go +var result osapi.PowerResult +err := results.Decode("shutdown-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Action` | `string` | Action taken (e.g., `shutdown`). | +| `Delay` | `int` | Delay in seconds before execution. | +| `Changed` | `bool` | Whether the shutdown was initiated. | +| `Error` | `string` | Error message if shutdown failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always initiates a shutdown. + +## Permissions + +Requires `power:execute` permission. + +## Example + +See +[`examples/operations/power.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/power.go) +for a complete working example. diff --git a/docs/operations/process/README.md b/docs/operations/process/README.md new file mode 100644 index 0000000..46edbcf --- /dev/null +++ b/docs/operations/process/README.md @@ -0,0 +1,24 @@ +# Process Management + +List, inspect, and signal running processes on target nodes. + +## Operations + +| Method | Description | Idempotent | +| -------------------------------------------------- | ---------------------------- | -------------- | +| [`ProcessList(target)`](list.md) | List running processes | Read-only | +| [`ProcessGet(target, pid)`](get.md) | Get a specific process | Read-only | +| [`ProcessSignal(target, pid, opts)`](signal.md) | Send a signal to a process | Non-idempotent | + +## Permissions + +| Operation | Permission | +| ----------------- | ----------------- | +| Read operations | `process:read` | +| Signal operations | `process:execute` | + +## Example + +See +[`examples/operations/process.go`](../../examples/operations/process.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/process/get.md b/docs/operations/process/get.md new file mode 100644 index 0000000..398e707 --- /dev/null +++ b/docs/operations/process/get.md @@ -0,0 +1,43 @@ +# ProcessGet + +Retrieves a specific process by PID from the target node. + +## Usage + +```go +step := o.ProcessGet("web-01", 1234) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `pid` | `int` | Process ID to retrieve. | + +## Result Type + +```go +var result osapi.ProcessInfoResult +err := results.Decode("get-process-1", &result) +``` + +| Field | Type | Description | +| ----------- | --------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Processes` | `[]ProcessInfo` | Process details (single entry). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `process:read` permission. + +## Example + +See +[`examples/operations/process.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/process.go) +for a complete working example. diff --git a/docs/operations/process/list.md b/docs/operations/process/list.md new file mode 100644 index 0000000..b5db847 --- /dev/null +++ b/docs/operations/process/list.md @@ -0,0 +1,56 @@ +# ProcessList + +Lists running processes on the target node. + +## Usage + +```go +step := o.ProcessList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.ProcessInfoResult +err := results.Decode("list-process-1", &result) +``` + +| Field | Type | Description | +| ----------- | --------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Processes` | `[]ProcessInfo` | List of running processes. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### ProcessInfo + +| Field | Type | Description | +| ------------ | --------- | -------------------------- | +| `PID` | `int` | Process ID. | +| `Name` | `string` | Process name. | +| `User` | `string` | User running the process. | +| `State` | `string` | Process state. | +| `CPUPercent` | `float64` | CPU usage percentage. | +| `MemPercent` | `float32` | Memory usage percentage. | +| `MemRSS` | `int64` | Resident set size (bytes). | +| `Command` | `string` | Full command line. | +| `StartTime` | `string` | Process start time. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `process:read` permission. + +## Example + +See +[`examples/operations/process.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/process.go) +for a complete working example. diff --git a/docs/operations/process/signal.md b/docs/operations/process/signal.md new file mode 100644 index 0000000..74b3b28 --- /dev/null +++ b/docs/operations/process/signal.md @@ -0,0 +1,53 @@ +# ProcessSignal + +Sends a signal to a process on the target node. + +## Usage + +```go +step := o.ProcessSignal("web-01", 1234, osapi.ProcessSignalOpts{ + Signal: "TERM", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `pid` | `int` | Process ID to signal. | +| `opts` | `ProcessSignalOpts` | Signal options (see below). | + +### ProcessSignalOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ------------------------------------------------ | +| `Signal` | `string` | Yes | Signal name (e.g., `TERM`, `KILL`, `HUP`). | + +## Result Type + +```go +var result osapi.ProcessSignalResult +err := results.Decode("signal-process-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `PID` | `int` | Process ID that was signaled. | +| `Signal` | `string` | Signal that was sent. | +| `Changed` | `bool` | Whether the signal was delivered. | +| `Error` | `string` | Error message if signal failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always delivers the signal regardless of current state. + +## Permissions + +Requires `process:execute` permission. + +## Example + +See +[`examples/operations/process.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/process.go) +for a complete working example. diff --git a/docs/operations/route/README.md b/docs/operations/route/README.md new file mode 100644 index 0000000..c8806e6 --- /dev/null +++ b/docs/operations/route/README.md @@ -0,0 +1,26 @@ +# Route Management + +Manage network route configuration via Netplan on target nodes -- list, get, +create, update, and delete routes. + +## Operations + +| Method | Description | Idempotent | +| ------------------------------------------------------------ | --------------------------- | -------------- | +| [`RouteList(target)`](list.md) | List network routes | Read-only | +| [`RouteGet(target, iface)`](get.md) | Get routes for an interface | Read-only | +| [`RouteCreate(target, iface, opts)`](create.md) | Create route configuration | Non-idempotent | +| [`RouteUpdate(target, iface, opts)`](update.md) | Update route configuration | Idempotent | +| [`RouteDelete(target, iface)`](delete.md) | Delete route configuration | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | --------------- | +| Read operations | `network:read` | +| Write operations | `network:write` | + +## Example + +See [`examples/operations/route.go`](../../examples/operations/route.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/route/create.md b/docs/operations/route/create.md new file mode 100644 index 0000000..606969d --- /dev/null +++ b/docs/operations/route/create.md @@ -0,0 +1,62 @@ +# RouteCreate + +Creates route configuration for a network interface on the target node. + +## Usage + +```go +step := o.RouteCreate("web-01", "eth0", osapi.RouteConfigOpts{ + Routes: []osapi.RouteItem{ + {To: "10.0.0.0/8", Via: "192.168.1.1"}, + }, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------------- | ----------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `interfaceName` | `string` | Network interface to add routes to. | +| `opts` | `RouteConfigOpts` | Route configuration (see below). | + +### RouteConfigOpts + +| Field | Type | Required | Description | +| -------- | ------------- | -------- | --------------------- | +| `Routes` | `[]RouteItem` | Yes | List of route entries. | + +### RouteItem + +| Field | Type | Required | Description | +| ----- | -------- | -------- | ----------------------------------- | +| `To` | `string` | Yes | Destination in CIDR notation. | +| `Via` | `string` | Yes | Gateway IP address. | + +## Result Type + +```go +var result osapi.RouteMutationResult +err := results.Decode("create-route-1", &result) +``` + +| Field | Type | Description | +| ----------- | -------- | --------------------------------------------------- | +| `Interface` | `string` | Interface name. | +| `Changed` | `bool` | Whether the routes were created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating routes on an interface that already has routes +returns an error. Use [RouteUpdate](update.md) to modify existing routes. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/route.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/route.go) +for a complete working example. diff --git a/docs/operations/route/delete.md b/docs/operations/route/delete.md new file mode 100644 index 0000000..cccc75b --- /dev/null +++ b/docs/operations/route/delete.md @@ -0,0 +1,43 @@ +# RouteDelete + +Deletes route configuration for a network interface on the target node. + +## Usage + +```go +step := o.RouteDelete("web-01", "eth0") +``` + +## Parameters + +| Parameter | Type | Description | +| --------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `interfaceName` | `string` | Network interface to delete routes from. | + +## Result Type + +```go +var result osapi.RouteMutationResult +err := results.Decode("delete-route-1", &result) +``` + +| Field | Type | Description | +| ----------- | -------- | ------------------------------------------------------ | +| `Interface` | `string` | Interface name. | +| `Changed` | `bool` | Whether the routes were removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If no routes exist for the interface, returns `Changed: false`. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/route.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/route.go) +for a complete working example. diff --git a/docs/operations/route/get.md b/docs/operations/route/get.md new file mode 100644 index 0000000..6dbd48e --- /dev/null +++ b/docs/operations/route/get.md @@ -0,0 +1,43 @@ +# RouteGet + +Retrieves routes for a specific network interface from the target node. + +## Usage + +```go +step := o.RouteGet("web-01", "eth0") +``` + +## Parameters + +| Parameter | Type | Description | +| --------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `interfaceName` | `string` | Network interface to retrieve routes for. | + +## Result Type + +```go +var result osapi.RouteGetResult +err := results.Decode("get-route-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Routes` | `[]RouteInfo` | List of routes for the interface. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `network:read` permission. + +## Example + +See +[`examples/operations/route.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/route.go) +for a complete working example. diff --git a/docs/operations/route/list.md b/docs/operations/route/list.md new file mode 100644 index 0000000..43abe47 --- /dev/null +++ b/docs/operations/route/list.md @@ -0,0 +1,42 @@ +# RouteList + +Lists network routes on the target node. + +## Usage + +```go +step := o.RouteList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.RouteListResult +err := results.Decode("list-route-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Routes` | `[]RouteInfo` | List of route entries. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `network:read` permission. + +## Example + +See +[`examples/operations/route.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/route.go) +for a complete working example. diff --git a/docs/operations/route/update.md b/docs/operations/route/update.md new file mode 100644 index 0000000..44b1bce --- /dev/null +++ b/docs/operations/route/update.md @@ -0,0 +1,49 @@ +# RouteUpdate + +Updates route configuration for a network interface on the target node. + +## Usage + +```go +step := o.RouteUpdate("web-01", "eth0", osapi.RouteConfigOpts{ + Routes: []osapi.RouteItem{ + {To: "10.0.0.0/8", Via: "192.168.1.254"}, + }, +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------------- | ----------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `interfaceName` | `string` | Network interface to update routes for. | +| `opts` | `RouteConfigOpts` | Route configuration (see [RouteCreate](create.md)). | + +## Result Type + +```go +var result osapi.RouteMutationResult +err := results.Decode("update-route-1", &result) +``` + +| Field | Type | Description | +| ----------- | -------- | ------------------------------------------------- | +| `Interface` | `string` | Interface name. | +| `Changed` | `bool` | Whether the routes were actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current routes against the desired configuration. +Returns `Changed: true` only if routes were actually modified. + +## Permissions + +Requires `network:write` permission. + +## Example + +See +[`examples/operations/route.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/route.go) +for a complete working example. diff --git a/docs/operations/service/README.md b/docs/operations/service/README.md new file mode 100644 index 0000000..5550812 --- /dev/null +++ b/docs/operations/service/README.md @@ -0,0 +1,32 @@ +# Service Management + +Manage systemd services on target nodes -- list, inspect, create and update unit +files, and control service lifecycle (start, stop, restart, enable, disable). + +## Operations + +| Method | Description | Idempotent | +| ------------------------------------------------------------ | --------------------------- | -------------- | +| [`ServiceList(target)`](list.md) | List services | Read-only | +| [`ServiceGet(target, name)`](get.md) | Get a specific service | Read-only | +| [`ServiceCreate(target, opts)`](create.md) | Create a service unit file | Non-idempotent | +| [`ServiceUpdate(target, name, opts)`](update.md) | Update a service unit file | Idempotent | +| [`ServiceDelete(target, name)`](delete.md) | Delete a service unit file | Idempotent | +| [`ServiceStart(target, name)`](start.md) | Start a service | Idempotent | +| [`ServiceStop(target, name)`](stop.md) | Stop a service | Idempotent | +| [`ServiceRestart(target, name)`](restart.md) | Restart a service | Non-idempotent | +| [`ServiceEnable(target, name)`](enable.md) | Enable a service on boot | Idempotent | +| [`ServiceDisable(target, name)`](disable.md) | Disable a service on boot | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | --------------- | +| Read operations | `service:read` | +| Write operations | `service:write` | + +## Example + +See +[`examples/operations/service.go`](../../examples/operations/service.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/service/create.md b/docs/operations/service/create.md new file mode 100644 index 0000000..50796af --- /dev/null +++ b/docs/operations/service/create.md @@ -0,0 +1,55 @@ +# ServiceCreate + +Creates a service unit file on the target node. The unit file must be uploaded +to the NATS Object Store first (see [FileUpload](../file/upload.md)). + +## Usage + +```go +step := o.ServiceCreate("web-01", osapi.ServiceCreateOpts{ + Name: "myapp", + Object: "myapp.service", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `ServiceCreateOpts` | Create options (see below). | + +### ServiceCreateOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ---------------------------------------------- | +| `Name` | `string` | Yes | Service unit name. | +| `Object` | `string` | Yes | Object Store reference for the unit file. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("create-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Name of the created service. | +| `Changed` | `bool` | Whether the unit file was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating a service that already exists returns an error. +Use [ServiceUpdate](update.md) to modify existing unit files. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/delete.md b/docs/operations/service/delete.md new file mode 100644 index 0000000..d7fb1a8 --- /dev/null +++ b/docs/operations/service/delete.md @@ -0,0 +1,44 @@ +# ServiceDelete + +Deletes a service unit file from the target node. + +## Usage + +```go +step := o.ServiceDelete("web-01", "myapp") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to delete. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("delete-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Name` | `string` | Name of the deleted service. | +| `Changed` | `bool` | Whether the unit file was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the service does not exist, the operation returns +`Changed: false`. If it exists and is removed, returns `Changed: true`. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/disable.md b/docs/operations/service/disable.md new file mode 100644 index 0000000..107a809 --- /dev/null +++ b/docs/operations/service/disable.md @@ -0,0 +1,43 @@ +# ServiceDisable + +Disables a service from starting on boot on the target node. + +## Usage + +```go +step := o.ServiceDisable("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to disable. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("disable-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Name of the disabled service. | +| `Changed` | `bool` | Whether the service was disabled. | +| `Error` | `string` | Error message if disable failed; empty on success. | + +## Idempotency + +**Idempotent.** If the service is already disabled, returns `Changed: false`. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/enable.md b/docs/operations/service/enable.md new file mode 100644 index 0000000..d464f98 --- /dev/null +++ b/docs/operations/service/enable.md @@ -0,0 +1,43 @@ +# ServiceEnable + +Enables a service to start on boot on the target node. + +## Usage + +```go +step := o.ServiceEnable("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to enable. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("enable-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Name of the enabled service. | +| `Changed` | `bool` | Whether the service was enabled. | +| `Error` | `string` | Error message if enable failed; empty on success. | + +## Idempotency + +**Idempotent.** If the service is already enabled, returns `Changed: false`. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/get.md b/docs/operations/service/get.md new file mode 100644 index 0000000..baa2c1d --- /dev/null +++ b/docs/operations/service/get.md @@ -0,0 +1,43 @@ +# ServiceGet + +Retrieves a specific service by name from the target node. + +## Usage + +```go +step := o.ServiceGet("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to retrieve. | + +## Result Type + +```go +var result osapi.ServiceGetResult +err := results.Decode("get-service-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Service` | `*ServiceInfo` | Service details (see ServiceList). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `service:read` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/list.md b/docs/operations/service/list.md new file mode 100644 index 0000000..87f7ed2 --- /dev/null +++ b/docs/operations/service/list.md @@ -0,0 +1,52 @@ +# ServiceList + +Lists all services on the target node. + +## Usage + +```go +step := o.ServiceList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.ServiceInfoResult +err := results.Decode("list-service-1", &result) +``` + +| Field | Type | Description | +| ---------- | --------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Services` | `[]ServiceInfo` | List of service entries. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### ServiceInfo + +| Field | Type | Description | +| ------------- | -------- | ------------------------------------ | +| `Name` | `string` | Service unit name. | +| `Status` | `string` | Active status of the service. | +| `Enabled` | `bool` | Whether the service starts on boot. | +| `Description` | `string` | Service description from unit file. | +| `PID` | `int` | Main process ID of the service. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `service:read` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/restart.md b/docs/operations/service/restart.md new file mode 100644 index 0000000..090cdc2 --- /dev/null +++ b/docs/operations/service/restart.md @@ -0,0 +1,43 @@ +# ServiceRestart + +Restarts a service on the target node. + +## Usage + +```go +step := o.ServiceRestart("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to restart. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("restart-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Name of the restarted service. | +| `Changed` | `bool` | Whether the service was restarted. | +| `Error` | `string` | Error message if restart failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always restarts the service regardless of current state. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/start.md b/docs/operations/service/start.md new file mode 100644 index 0000000..2ab23d9 --- /dev/null +++ b/docs/operations/service/start.md @@ -0,0 +1,43 @@ +# ServiceStart + +Starts a service on the target node. + +## Usage + +```go +step := o.ServiceStart("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to start. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("start-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Name of the started service. | +| `Changed` | `bool` | Whether the service was started. | +| `Error` | `string` | Error message if start failed; empty on success. | + +## Idempotency + +**Idempotent.** If the service is already running, returns `Changed: false`. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/stop.md b/docs/operations/service/stop.md new file mode 100644 index 0000000..d2cc492 --- /dev/null +++ b/docs/operations/service/stop.md @@ -0,0 +1,43 @@ +# ServiceStop + +Stops a service on the target node. + +## Usage + +```go +step := o.ServiceStop("web-01", "nginx") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to stop. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("stop-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------ | +| `Name` | `string` | Name of the stopped service. | +| `Changed` | `bool` | Whether the service was stopped. | +| `Error` | `string` | Error message if stop failed; empty on success. | + +## Idempotency + +**Idempotent.** If the service is already stopped, returns `Changed: false`. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/service/update.md b/docs/operations/service/update.md new file mode 100644 index 0000000..b9644e0 --- /dev/null +++ b/docs/operations/service/update.md @@ -0,0 +1,53 @@ +# ServiceUpdate + +Updates a service unit file on the target node. + +## Usage + +```go +step := o.ServiceUpdate("web-01", "myapp", osapi.ServiceUpdateOpts{ + Object: "myapp-v2.service", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | ------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `serviceName` | `string` | Name of the service to update. | +| `opts` | `ServiceUpdateOpts` | Update options (see below). | + +### ServiceUpdateOpts + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ---------------------------------------------- | +| `Object` | `string` | Yes | New Object Store reference for the unit file. | + +## Result Type + +```go +var result osapi.ServiceMutationResult +err := results.Decode("update-service-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Name of the updated service. | +| `Changed` | `bool` | Whether the unit file was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current unit file against the new content. Returns +`Changed: true` only if the file was actually modified. + +## Permissions + +Requires `service:write` permission. + +## Example + +See +[`examples/operations/service.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/service.go) +for a complete working example. diff --git a/docs/operations/sysctl/README.md b/docs/operations/sysctl/README.md new file mode 100644 index 0000000..6041b73 --- /dev/null +++ b/docs/operations/sysctl/README.md @@ -0,0 +1,27 @@ +# Sysctl Management + +Manage kernel parameters via sysctl drop-in files on target nodes -- create, +update, list, get, and delete parameters backed by the file provider. + +## Operations + +| Method | Description | Idempotent | +| ---------------------------------------------- | ----------------------------------- | -------------- | +| [`SysctlList(target)`](list.md) | List managed sysctl parameters | Read-only | +| [`SysctlGet(target, key)`](get.md) | Get a specific sysctl parameter | Read-only | +| [`SysctlCreate(target, opts)`](create.md) | Create a new sysctl parameter | Non-idempotent | +| [`SysctlUpdate(target, key, opts)`](update.md) | Update an existing sysctl parameter | Idempotent | +| [`SysctlDelete(target, key)`](delete.md) | Delete a sysctl parameter | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | -------------- | +| Read operations | `sysctl:read` | +| Write operations | `sysctl:write` | + +## Example + +See +[`examples/operations/sysctl.go`](../../examples/operations/sysctl.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/sysctl/create.md b/docs/operations/sysctl/create.md new file mode 100644 index 0000000..06daad7 --- /dev/null +++ b/docs/operations/sysctl/create.md @@ -0,0 +1,54 @@ +# SysctlCreate + +Creates a new sysctl parameter on the target node. + +## Usage + +```go +step := o.SysctlCreate("web-01", osapi.SysctlCreateOpts{ + Key: "net.ipv4.ip_forward", + Value: "1", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ------------------ | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `SysctlCreateOpts` | Create options (see below). | + +### SysctlCreateOpts + +| Field | Type | Required | Description | +| ------- | -------- | -------- | ------------------------------------------------- | +| `Key` | `string` | Yes | Sysctl parameter key (e.g., `net.ipv4.ip_forward`). | +| `Value` | `string` | Yes | Value to set for the parameter. | + +## Result Type + +```go +var result osapi.SysctlMutationResult +err := results.Decode("create-sysctl-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Key` | `string` | Sysctl parameter key. | +| `Changed` | `bool` | Whether the parameter was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating a parameter that already exists returns an error. +Use [SysctlUpdate](update.md) to modify existing parameters. + +## Permissions + +Requires `sysctl:write` permission. + +## Example + +See +[`examples/operations/sysctl.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/sysctl.go) +for a complete working example. diff --git a/docs/operations/sysctl/delete.md b/docs/operations/sysctl/delete.md new file mode 100644 index 0000000..1e09eff --- /dev/null +++ b/docs/operations/sysctl/delete.md @@ -0,0 +1,45 @@ +# SysctlDelete + +Deletes a sysctl parameter from the target node. + +## Usage + +```go +step := o.SysctlDelete("web-01", "net.ipv4.ip_forward") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `key` | `string` | Sysctl parameter key to delete. | + +## Result Type + +```go +var result osapi.SysctlMutationResult +err := results.Decode("delete-sysctl-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Key` | `string` | Sysctl parameter key. | +| `Changed` | `bool` | Whether the parameter was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the parameter does not exist, the operation returns +`Changed: false`. If the parameter exists and is removed, returns +`Changed: true`. + +## Permissions + +Requires `sysctl:write` permission. + +## Example + +See +[`examples/operations/sysctl.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/sysctl.go) +for a complete working example. diff --git a/docs/operations/sysctl/get.md b/docs/operations/sysctl/get.md new file mode 100644 index 0000000..540714b --- /dev/null +++ b/docs/operations/sysctl/get.md @@ -0,0 +1,44 @@ +# SysctlGet + +Retrieves a specific sysctl parameter by key from the target node. + +## Usage + +```go +step := o.SysctlGet("web-01", "net.ipv4.ip_forward") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `key` | `string` | Sysctl parameter key to retrieve. | + +## Result Type + +```go +var result osapi.SysctlEntryResult +err := results.Decode("get-sysctl-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Key` | `string` | Sysctl parameter key. | +| `Value` | `string` | Current value of the parameter. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `sysctl:read` permission. + +## Example + +See +[`examples/operations/sysctl.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/sysctl.go) +for a complete working example. diff --git a/docs/operations/sysctl/list.md b/docs/operations/sysctl/list.md new file mode 100644 index 0000000..b192a06 --- /dev/null +++ b/docs/operations/sysctl/list.md @@ -0,0 +1,43 @@ +# SysctlList + +Lists all managed sysctl parameters on the target node. + +## Usage + +```go +step := o.SysctlList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.SysctlEntryResult +err := results.Decode("list-sysctl-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Key` | `string` | Sysctl parameter key. | +| `Value` | `string` | Current value of the parameter. | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `sysctl:read` permission. + +## Example + +See +[`examples/operations/sysctl.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/sysctl.go) +for a complete working example. diff --git a/docs/operations/sysctl/update.md b/docs/operations/sysctl/update.md new file mode 100644 index 0000000..7154433 --- /dev/null +++ b/docs/operations/sysctl/update.md @@ -0,0 +1,54 @@ +# SysctlUpdate + +Updates an existing sysctl parameter on the target node. + +## Usage + +```go +step := o.SysctlUpdate("web-01", "net.ipv4.ip_forward", osapi.SysctlUpdateOpts{ + Value: "0", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ------------------ | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `key` | `string` | Sysctl parameter key to update. | +| `opts` | `SysctlUpdateOpts` | Update options (see below). | + +### SysctlUpdateOpts + +| Field | Type | Required | Description | +| ------- | -------- | -------- | ------------------------- | +| `Value` | `string` | Yes | New value for the parameter. | + +## Result Type + +```go +var result osapi.SysctlMutationResult +err := results.Decode("update-sysctl-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Key` | `string` | Sysctl parameter key. | +| `Changed` | `bool` | Whether the parameter was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current value against the desired value. Returns +`Changed: true` only if the parameter was actually modified. If the value +already matches, returns `Changed: false`. + +## Permissions + +Requires `sysctl:write` permission. + +## Example + +See +[`examples/operations/sysctl.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/sysctl.go) +for a complete working example. diff --git a/docs/operations/timezone/README.md b/docs/operations/timezone/README.md new file mode 100644 index 0000000..4ae944e --- /dev/null +++ b/docs/operations/timezone/README.md @@ -0,0 +1,24 @@ +# Timezone Management + +Manage the system timezone on target nodes -- get the current timezone and +update it. + +## Operations + +| Method | Description | Idempotent | +| ----------------------------------------------- | ------------------------ | ---------- | +| [`TimezoneGet(target)`](get.md) | Get the system timezone | Read-only | +| [`TimezoneUpdate(target, opts)`](update.md) | Set the system timezone | Idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | ---------------- | +| Read operations | `timezone:read` | +| Write operations | `timezone:write` | + +## Example + +See +[`examples/operations/timezone.go`](../../examples/operations/timezone.go) for +a complete workflow example. diff --git a/docs/operations/timezone/get.md b/docs/operations/timezone/get.md new file mode 100644 index 0000000..10bb723 --- /dev/null +++ b/docs/operations/timezone/get.md @@ -0,0 +1,43 @@ +# TimezoneGet + +Retrieves the system timezone from the target node. + +## Usage + +```go +step := o.TimezoneGet("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.TimezoneResult +err := results.Decode("get-timezone-1", &result) +``` + +| Field | Type | Description | +| ----------- | -------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Timezone` | `string` | IANA timezone name (e.g., `America/New_York`). | +| `UTCOffset` | `string` | UTC offset string (e.g., `-0500`). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `timezone:read` permission. + +## Example + +See +[`examples/operations/timezone.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/timezone.go) +for a complete working example. diff --git a/docs/operations/timezone/update.md b/docs/operations/timezone/update.md new file mode 100644 index 0000000..2aea85e --- /dev/null +++ b/docs/operations/timezone/update.md @@ -0,0 +1,52 @@ +# TimezoneUpdate + +Sets the system timezone on the target node. + +## Usage + +```go +step := o.TimezoneUpdate("web-01", osapi.TimezoneUpdateOpts{ + Timezone: "America/New_York", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `TimezoneUpdateOpts` | Update options (see below). | + +### TimezoneUpdateOpts + +| Field | Type | Required | Description | +| ---------- | -------- | -------- | -------------------------------------------------- | +| `Timezone` | `string` | Yes | IANA timezone name (e.g., `America/New_York`, `UTC`). | + +## Result Type + +```go +var result osapi.TimezoneMutationResult +err := results.Decode("update-timezone-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------- | ------------------------------------------------- | +| `Timezone` | `string` | The timezone that was set. | +| `Changed` | `bool` | Whether the timezone was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current timezone against the desired value. +Returns `Changed: true` only if the timezone was actually modified. + +## Permissions + +Requires `timezone:write` permission. + +## Example + +See +[`examples/operations/timezone.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/timezone.go) +for a complete working example. diff --git a/docs/operations/user/README.md b/docs/operations/user/README.md new file mode 100644 index 0000000..ba6d86e --- /dev/null +++ b/docs/operations/user/README.md @@ -0,0 +1,30 @@ +# User Management + +Manage local user accounts and SSH authorized keys on target nodes -- list, get, +create, update, delete users, and manage their SSH keys and passwords. + +## Operations + +| Method | Description | Idempotent | +| ------------------------------------------------------------------------- | ---------------------------- | -------------- | +| [`UserList(target)`](list.md) | List user accounts | Read-only | +| [`UserGet(target, username)`](get.md) | Get a specific user | Read-only | +| [`UserCreate(target, opts)`](create.md) | Create a user account | Non-idempotent | +| [`UserUpdate(target, username, opts)`](update.md) | Update a user account | Idempotent | +| [`UserDelete(target, username)`](delete.md) | Delete a user account | Idempotent | +| [`UserListKeys(target, username)`](list-keys.md) | List SSH authorized keys | Read-only | +| [`UserAddKey(target, username, opts)`](add-key.md) | Add an SSH authorized key | Non-idempotent | +| [`UserRemoveKey(target, username, fingerprint)`](remove-key.md) | Remove an SSH authorized key | Idempotent | +| [`UserChangePassword(target, username, password)`](change-password.md) | Change a user's password | Non-idempotent | + +## Permissions + +| Operation | Permission | +| ---------------- | ------------ | +| Read operations | `user:read` | +| Write operations | `user:write` | + +## Example + +See [`examples/operations/user.go`](../../examples/operations/user.go) for a +complete workflow example covering all operations. diff --git a/docs/operations/user/add-key.md b/docs/operations/user/add-key.md new file mode 100644 index 0000000..3daa6eb --- /dev/null +++ b/docs/operations/user/add-key.md @@ -0,0 +1,51 @@ +# UserAddKey + +Adds an SSH authorized key for a user on the target node. + +## Usage + +```go +step := o.UserAddKey("web-01", "deploy", osapi.SSHKeyAddOpts{ + Key: "ssh-ed25519 AAAA... user@host", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | --------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username to add the key for. | +| `opts` | `SSHKeyAddOpts` | Add key options (see below). | + +### SSHKeyAddOpts + +| Field | Type | Required | Description | +| ----- | -------- | -------- | -------------------------------------------- | +| `Key` | `string` | Yes | Full SSH public key line. | + +## Result Type + +```go +var result osapi.SSHKeyMutationResult +err := results.Decode("add-ssh-key-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Changed` | `bool` | Whether the key was added. | +| `Error` | `string` | Error message if add failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Adding a key that already exists returns an error. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/change-password.md b/docs/operations/user/change-password.md new file mode 100644 index 0000000..3698bba --- /dev/null +++ b/docs/operations/user/change-password.md @@ -0,0 +1,44 @@ +# UserChangePassword + +Changes a user's password on the target node. + +## Usage + +```go +step := o.UserChangePassword("web-01", "deploy", "newpassword123") +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username whose password to change. | +| `password` | `string` | New password. | + +## Result Type + +```go +var result osapi.UserMutationResult +err := results.Decode("change-password-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Username. | +| `Changed` | `bool` | Whether the password was changed. | +| `Error` | `string` | Error message if change failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Always sets the password regardless of current state. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/create.md b/docs/operations/user/create.md new file mode 100644 index 0000000..60f61bb --- /dev/null +++ b/docs/operations/user/create.md @@ -0,0 +1,57 @@ +# UserCreate + +Creates a user account on the target node. + +## Usage + +```go +step := o.UserCreate("web-01", osapi.UserCreateOpts{ + Name: "deploy", + Shell: "/bin/bash", + Home: "/home/deploy", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | ---------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `opts` | `UserCreateOpts` | Create options (see below). | + +### UserCreateOpts + +| Field | Type | Required | Description | +| ------- | -------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Yes | Username. | +| `UID` | `int` | No | Numeric user ID (system assigns if zero). | +| `GID` | `int` | No | Primary group ID (creates matching group if zero). | +| `Home` | `string` | No | Home directory path. | +| `Shell` | `string` | No | Login shell path. | + +## Result Type + +```go +var result osapi.UserMutationResult +err := results.Decode("create-user-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Username. | +| `Changed` | `bool` | Whether the user was created. | +| `Error` | `string` | Error message if creation failed; empty on success. | + +## Idempotency + +**Non-idempotent.** Creating a user that already exists returns an error. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/delete.md b/docs/operations/user/delete.md new file mode 100644 index 0000000..6ff3687 --- /dev/null +++ b/docs/operations/user/delete.md @@ -0,0 +1,43 @@ +# UserDelete + +Deletes a user account from the target node. + +## Usage + +```go +step := o.UserDelete("web-01", "deploy") +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username to delete. | + +## Result Type + +```go +var result osapi.UserMutationResult +err := results.Decode("delete-user-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Username. | +| `Changed` | `bool` | Whether the user was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | + +## Idempotency + +**Idempotent.** If the user does not exist, returns `Changed: false`. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/get.md b/docs/operations/user/get.md new file mode 100644 index 0000000..d53bdcc --- /dev/null +++ b/docs/operations/user/get.md @@ -0,0 +1,43 @@ +# UserGet + +Retrieves a specific user account by username. + +## Usage + +```go +step := o.UserGet("web-01", "deploy") +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username to retrieve. | + +## Result Type + +```go +var result osapi.UserInfoResult +err := results.Decode("get-user-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------ | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Users` | `[]UserInfo` | User details (single entry). | +| `Error` | `string` | Error message if query failed; empty on success. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `user:read` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/list-keys.md b/docs/operations/user/list-keys.md new file mode 100644 index 0000000..ce40cb7 --- /dev/null +++ b/docs/operations/user/list-keys.md @@ -0,0 +1,51 @@ +# UserListKeys + +Lists SSH authorized keys for a user on the target node. + +## Usage + +```go +step := o.UserListKeys("web-01", "deploy") +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username whose SSH keys to list. | + +## Result Type + +```go +var result osapi.SSHKeyInfoResult +err := results.Decode("list-ssh-key-1", &result) +``` + +| Field | Type | Description | +| ---------- | -------------- | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Keys` | `[]SSHKeyInfo` | List of SSH authorized keys. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### SSHKeyInfo + +| Field | Type | Description | +| ------------- | -------- | ---------------------- | +| `Type` | `string` | Key type (e.g., `ssh-ed25519`). | +| `Fingerprint` | `string` | Key fingerprint. | +| `Comment` | `string` | Key comment. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `user:read` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/list.md b/docs/operations/user/list.md new file mode 100644 index 0000000..06027b4 --- /dev/null +++ b/docs/operations/user/list.md @@ -0,0 +1,54 @@ +# UserList + +Lists user accounts on the target node. + +## Usage + +```go +step := o.UserList("web-01") +``` + +## Parameters + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | + +## Result Type + +```go +var result osapi.UserInfoResult +err := results.Decode("list-user-1", &result) +``` + +| Field | Type | Description | +| ---------- | ------------ | ------------------------------------------------ | +| `Hostname` | `string` | The node's hostname. | +| `Users` | `[]UserInfo` | List of user accounts. | +| `Error` | `string` | Error message if query failed; empty on success. | + +### UserInfo + +| Field | Type | Description | +| -------- | ---------- | -------------------------- | +| `Name` | `string` | Username. | +| `UID` | `int` | Numeric user ID. | +| `GID` | `int` | Primary group ID. | +| `Home` | `string` | Home directory path. | +| `Shell` | `string` | Login shell path. | +| `Groups` | `[]string` | Supplementary group names. | +| `Locked` | `bool` | Whether the account is locked. | + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `user:read` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/remove-key.md b/docs/operations/user/remove-key.md new file mode 100644 index 0000000..53ad9e1 --- /dev/null +++ b/docs/operations/user/remove-key.md @@ -0,0 +1,43 @@ +# UserRemoveKey + +Removes an SSH authorized key from a user on the target node. + +## Usage + +```go +step := o.UserRemoveKey("web-01", "deploy", "SHA256:abc123...") +``` + +## Parameters + +| Parameter | Type | Description | +| ------------- | -------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username to remove the key from. | +| `fingerprint` | `string` | Fingerprint of the key to remove. | + +## Result Type + +```go +var result osapi.SSHKeyMutationResult +err := results.Decode("remove-ssh-key-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Changed` | `bool` | Whether the key was removed or already absent. | +| `Error` | `string` | Error message if removal failed; empty on success. | + +## Idempotency + +**Idempotent.** If the key does not exist, returns `Changed: false`. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. diff --git a/docs/operations/user/update.md b/docs/operations/user/update.md new file mode 100644 index 0000000..6385f02 --- /dev/null +++ b/docs/operations/user/update.md @@ -0,0 +1,57 @@ +# UserUpdate + +Updates a user account on the target node. Only the fields specified in the +options are changed. + +## Usage + +```go +step := o.UserUpdate("web-01", "deploy", osapi.UserUpdateOpts{ + Shell: "/bin/zsh", +}) +``` + +## Parameters + +| Parameter | Type | Description | +| ---------- | ---------------- | --------------------------------------------------------- | +| `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | +| `username` | `string` | Username to update. | +| `opts` | `UserUpdateOpts` | Update options (see below). | + +### UserUpdateOpts + +| Field | Type | Required | Description | +| -------- | ---------- | -------- | ------------------------------------------------- | +| `Shell` | `string` | No | New login shell path. | +| `Home` | `string` | No | New home directory path. | +| `Groups` | `[]string` | No | Supplementary group names (replaces existing). | +| `Lock` | `*bool` | No | Lock or unlock the account. | + +## Result Type + +```go +var result osapi.UserMutationResult +err := results.Decode("update-user-1", &result) +``` + +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Username. | +| `Changed` | `bool` | Whether the user was actually modified. | +| `Error` | `string` | Error message if update failed; empty on success. | + +## Idempotency + +**Idempotent.** Compares the current account against the desired state. Returns +`Changed: true` only if the account was actually modified. + +## Permissions + +Requires `user:write` permission. + +## Example + +See +[`examples/operations/user.go`](https://github.com/osapi-io/osapi-orchestrator/blob/main/examples/operations/user.go) +for a complete working example. From 4fb2468e5ca9a3fb16d4d62591e0e6e8fec13c25 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: Mon, 6 Apr 2026 13:03:56 -0700 Subject: [PATCH 3/4] test: add 100% coverage for all new operation constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal (httptest round-trip) and public (step-creation) tests for all 64 previously uncovered operations across interface, route, sysctl, NTP, timezone, service, package, user, group, certificate, process, power, and log domains. Update operation docs formatting. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- docs/operations/README.md | 46 +- docs/operations/certificate/README.md | 12 +- docs/operations/certificate/create.md | 12 +- docs/operations/certificate/delete.md | 10 +- docs/operations/certificate/list.md | 8 +- docs/operations/certificate/update.md | 6 +- docs/operations/group/README.md | 14 +- docs/operations/group/create.md | 10 +- docs/operations/group/delete.md | 10 +- docs/operations/group/list.md | 10 +- docs/operations/group/update.md | 6 +- docs/operations/interface/README.md | 19 +- docs/operations/interface/create.md | 16 +- docs/operations/interface/delete.md | 10 +- docs/operations/log/README.md | 16 +- docs/operations/log/query-unit.md | 2 +- docs/operations/log/query.md | 26 +- docs/operations/network/dns-delete.md | 6 +- docs/operations/ntp/README.md | 12 +- docs/operations/ntp/create.md | 10 +- docs/operations/ntp/delete.md | 6 +- docs/operations/ntp/update.md | 6 +- docs/operations/package/README.md | 21 +- docs/operations/package/install.md | 10 +- docs/operations/package/list-updates.md | 8 +- docs/operations/package/list.md | 14 +- docs/operations/package/remove.md | 10 +- docs/operations/package/update.md | 8 +- docs/operations/power/README.md | 14 +- docs/operations/power/reboot.md | 20 +- docs/operations/power/shutdown.md | 20 +- docs/operations/process/README.md | 15 +- docs/operations/process/signal.md | 18 +- docs/operations/route/README.md | 14 +- docs/operations/route/create.md | 12 +- docs/operations/route/delete.md | 10 +- docs/operations/service/README.md | 29 +- docs/operations/service/create.md | 12 +- docs/operations/service/disable.md | 10 +- docs/operations/service/enable.md | 10 +- docs/operations/service/list.md | 14 +- docs/operations/service/restart.md | 10 +- docs/operations/service/start.md | 10 +- docs/operations/service/stop.md | 10 +- docs/operations/service/update.md | 6 +- docs/operations/sysctl/README.md | 3 +- docs/operations/sysctl/create.md | 6 +- docs/operations/sysctl/delete.md | 8 +- docs/operations/sysctl/update.md | 4 +- docs/operations/timezone/README.md | 13 +- docs/operations/timezone/update.md | 8 +- docs/operations/user/README.md | 22 +- docs/operations/user/add-key.md | 14 +- docs/operations/user/create.md | 14 +- docs/operations/user/list-keys.md | 8 +- docs/operations/user/list.md | 16 +- docs/operations/user/remove-key.md | 8 +- docs/operations/user/update.md | 12 +- pkg/orchestrator/ops.go | 24 +- pkg/orchestrator/ops_public_test.go | 1344 ++++++++ pkg/orchestrator/ops_test.go | 4224 +++++++++++++++++++++++ 61 files changed, 5934 insertions(+), 372 deletions(-) diff --git a/docs/operations/README.md b/docs/operations/README.md index 9a0c90f..3b8d3cd 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -6,29 +6,29 @@ error handling. ## Domains -| Domain | Operations | Description | -| ----------------------------- | ---------- | ------------------------------------------------- | -| [Node](node/) | 8 | Hostname, disk, memory, load, uptime, OS, status | -| [Network](network/) | 4 | DNS configuration, ping | -| [Interface](interface/) | 5 | Network interface configuration via Netplan | -| [Route](route/) | 5 | Network route configuration via Netplan | -| [Command](command/) | 2 | Direct exec, shell commands | -| [Docker](docker/) | 9 | Container lifecycle, exec, images | -| [Cron](cron/) | 5 | Cron drop-in file management | -| [Sysctl](sysctl/) | 5 | Kernel parameter management | -| [NTP](ntp/) | 4 | NTP server configuration | -| [Timezone](timezone/) | 2 | System timezone management | -| [Service](service/) | 10 | Systemd service lifecycle and unit files | -| [Package](package/) | 6 | System package management | -| [User](user/) | 9 | User accounts, SSH keys, passwords | -| [Group](group/) | 5 | Local group management | -| [Certificate](certificate/) | 4 | CA certificate trust store management | -| [Process](process/) | 3 | Process listing, inspection, signals | -| [Power](power/) | 2 | Reboot and shutdown | -| [Log](log/) | 3 | Systemd journal queries | -| [File](file/) | 5 | Upload, deploy, status, undeploy, drift detection | -| [Agent](agent/) | 4 | Discovery, inspection, drain/undrain | -| [Health](health/) | 1 | Liveness check | +| Domain | Operations | Description | +| --------------------------- | ---------- | ------------------------------------------------- | +| [Node](node/) | 8 | Hostname, disk, memory, load, uptime, OS, status | +| [Network](network/) | 4 | DNS configuration, ping | +| [Interface](interface/) | 5 | Network interface configuration via Netplan | +| [Route](route/) | 5 | Network route configuration via Netplan | +| [Command](command/) | 2 | Direct exec, shell commands | +| [Docker](docker/) | 9 | Container lifecycle, exec, images | +| [Cron](cron/) | 5 | Cron drop-in file management | +| [Sysctl](sysctl/) | 5 | Kernel parameter management | +| [NTP](ntp/) | 4 | NTP server configuration | +| [Timezone](timezone/) | 2 | System timezone management | +| [Service](service/) | 10 | Systemd service lifecycle and unit files | +| [Package](package/) | 6 | System package management | +| [User](user/) | 9 | User accounts, SSH keys, passwords | +| [Group](group/) | 5 | Local group management | +| [Certificate](certificate/) | 4 | CA certificate trust store management | +| [Process](process/) | 3 | Process listing, inspection, signals | +| [Power](power/) | 2 | Reboot and shutdown | +| [Log](log/) | 3 | Systemd journal queries | +| [File](file/) | 5 | Upload, deploy, status, undeploy, drift detection | +| [Agent](agent/) | 4 | Discovery, inspection, drain/undrain | +| [Health](health/) | 1 | Liveness check | ## Idempotency diff --git a/docs/operations/certificate/README.md b/docs/operations/certificate/README.md index ec40d54..6767549 100644 --- a/docs/operations/certificate/README.md +++ b/docs/operations/certificate/README.md @@ -5,12 +5,12 @@ create, update, and delete certificates backed by the file provider. ## Operations -| Method | Description | Idempotent | -| ------------------------------------------------------------- | ------------------------- | -------------- | -| [`CertificateList(target)`](list.md) | List CA certificates | Read-only | -| [`CertificateCreate(target, opts)`](create.md) | Create a CA certificate | Non-idempotent | -| [`CertificateUpdate(target, name, opts)`](update.md) | Update a CA certificate | Idempotent | -| [`CertificateDelete(target, name)`](delete.md) | Delete a CA certificate | Idempotent | +| Method | Description | Idempotent | +| ---------------------------------------------------- | ----------------------- | -------------- | +| [`CertificateList(target)`](list.md) | List CA certificates | Read-only | +| [`CertificateCreate(target, opts)`](create.md) | Create a CA certificate | Non-idempotent | +| [`CertificateUpdate(target, name, opts)`](update.md) | Update a CA certificate | Idempotent | +| [`CertificateDelete(target, name)`](delete.md) | Delete a CA certificate | Idempotent | ## Permissions diff --git a/docs/operations/certificate/create.md b/docs/operations/certificate/create.md index 101e1ce..64b641b 100644 --- a/docs/operations/certificate/create.md +++ b/docs/operations/certificate/create.md @@ -21,10 +21,10 @@ step := o.CertificateCreate("web-01", osapi.CertificateCreateOpts{ ### CertificateCreateOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | ---------------------------------------------- | -| `Name` | `string` | Yes | Certificate name. | -| `Object` | `string` | Yes | Object Store reference for the PEM file. | +| Field | Type | Required | Description | +| -------- | -------- | -------- | ---------------------------------------- | +| `Name` | `string` | Yes | Certificate name. | +| `Object` | `string` | Yes | Object Store reference for the PEM file. | ## Result Type @@ -41,8 +41,8 @@ err := results.Decode("create-certificate-1", &result) ## Idempotency -**Non-idempotent.** Creating a certificate that already exists returns an -error. Use [CertificateUpdate](update.md) to replace existing certificates. +**Non-idempotent.** Creating a certificate that already exists returns an error. +Use [CertificateUpdate](update.md) to replace existing certificates. ## Permissions diff --git a/docs/operations/certificate/delete.md b/docs/operations/certificate/delete.md index cd5e04b..19e1aad 100644 --- a/docs/operations/certificate/delete.md +++ b/docs/operations/certificate/delete.md @@ -22,11 +22,11 @@ var result osapi.CertificateCAMutationResult err := results.Decode("delete-certificate-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | -------------------------------------------------------- | -| `Name` | `string` | Certificate name. | -| `Changed` | `bool` | Whether the certificate was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------------ | +| `Name` | `string` | Certificate name. | +| `Changed` | `bool` | Whether the certificate was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/certificate/list.md b/docs/operations/certificate/list.md index a9bf551..719aafa 100644 --- a/docs/operations/certificate/list.md +++ b/docs/operations/certificate/list.md @@ -29,11 +29,11 @@ err := results.Decode("list-certificate-1", &result) ### CertificateCA -| Field | Type | Description | -| -------- | -------- | --------------------------------------- | -| `Name` | `string` | Certificate name. | +| Field | Type | Description | +| -------- | -------- | ------------------------------------------ | +| `Name` | `string` | Certificate name. | | `Source` | `string` | Path to the certificate on the filesystem. | -| `Object` | `string` | Object Store reference. | +| `Object` | `string` | Object Store reference. | ## Idempotency diff --git a/docs/operations/certificate/update.md b/docs/operations/certificate/update.md index 823036d..74cdc19 100644 --- a/docs/operations/certificate/update.md +++ b/docs/operations/certificate/update.md @@ -20,9 +20,9 @@ step := o.CertificateUpdate("web-01", "internal-ca", osapi.CertificateUpdateOpts ### CertificateUpdateOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | ---------------------------------------------- | -| `Object` | `string` | Yes | New Object Store reference for the PEM file. | +| Field | Type | Required | Description | +| -------- | -------- | -------- | -------------------------------------------- | +| `Object` | `string` | Yes | New Object Store reference for the PEM file. | ## Result Type diff --git a/docs/operations/group/README.md b/docs/operations/group/README.md index 51078fc..8237a25 100644 --- a/docs/operations/group/README.md +++ b/docs/operations/group/README.md @@ -5,13 +5,13 @@ groups. ## Operations -| Method | Description | Idempotent | -| --------------------------------------------------------- | ---------------------- | -------------- | -| [`GroupList(target)`](list.md) | List groups | Read-only | -| [`GroupGet(target, name)`](get.md) | Get a specific group | Read-only | -| [`GroupCreate(target, opts)`](create.md) | Create a group | Non-idempotent | -| [`GroupUpdate(target, name, opts)`](update.md) | Update a group | Idempotent | -| [`GroupDelete(target, name)`](delete.md) | Delete a group | Idempotent | +| Method | Description | Idempotent | +| ---------------------------------------------- | -------------------- | -------------- | +| [`GroupList(target)`](list.md) | List groups | Read-only | +| [`GroupGet(target, name)`](get.md) | Get a specific group | Read-only | +| [`GroupCreate(target, opts)`](create.md) | Create a group | Non-idempotent | +| [`GroupUpdate(target, name, opts)`](update.md) | Update a group | Idempotent | +| [`GroupDelete(target, name)`](delete.md) | Delete a group | Idempotent | ## Permissions diff --git a/docs/operations/group/create.md b/docs/operations/group/create.md index 9f3ec45..d3d1ba1 100644 --- a/docs/operations/group/create.md +++ b/docs/operations/group/create.md @@ -19,11 +19,11 @@ step := o.GroupCreate("web-01", osapi.GroupCreateOpts{ ### GroupCreateOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | -------------------------------------------- | -| `Name` | `string` | Yes | Group name. | -| `GID` | `int` | No | Numeric group ID (system assigns if zero). | -| `System` | `bool` | No | Create a system group. | +| Field | Type | Required | Description | +| -------- | -------- | -------- | ------------------------------------------ | +| `Name` | `string` | Yes | Group name. | +| `GID` | `int` | No | Numeric group ID (system assigns if zero). | +| `System` | `bool` | No | Create a system group. | ## Result Type diff --git a/docs/operations/group/delete.md b/docs/operations/group/delete.md index e1b9c5d..7d03b00 100644 --- a/docs/operations/group/delete.md +++ b/docs/operations/group/delete.md @@ -22,11 +22,11 @@ var result osapi.GroupMutationResult err := results.Decode("delete-group-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------- | -| `Name` | `string` | Group name. | -| `Changed` | `bool` | Whether the group was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Name` | `string` | Group name. | +| `Changed` | `bool` | Whether the group was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/group/list.md b/docs/operations/group/list.md index d854775..ccd7d59 100644 --- a/docs/operations/group/list.md +++ b/docs/operations/group/list.md @@ -29,11 +29,11 @@ err := results.Decode("list-group-1", &result) ### GroupInfo -| Field | Type | Description | -| --------- | ---------- | --------------------- | -| `Name` | `string` | Group name. | -| `GID` | `int` | Numeric group ID. | -| `Members` | `[]string` | Group member names. | +| Field | Type | Description | +| --------- | ---------- | ------------------- | +| `Name` | `string` | Group name. | +| `GID` | `int` | Numeric group ID. | +| `Members` | `[]string` | Group member names. | ## Idempotency diff --git a/docs/operations/group/update.md b/docs/operations/group/update.md index 5e6fa25..fb34d46 100644 --- a/docs/operations/group/update.md +++ b/docs/operations/group/update.md @@ -20,9 +20,9 @@ step := o.GroupUpdate("web-01", "developers", osapi.GroupUpdateOpts{ ### GroupUpdateOpts -| Field | Type | Required | Description | -| --------- | ---------- | -------- | ---------------------------------------------- | -| `Members` | `[]string` | No | Group member usernames (replaces existing). | +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ------------------------------------------- | +| `Members` | `[]string` | No | Group member usernames (replaces existing). | ## Result Type diff --git a/docs/operations/interface/README.md b/docs/operations/interface/README.md index 312f764..de862bb 100644 --- a/docs/operations/interface/README.md +++ b/docs/operations/interface/README.md @@ -5,13 +5,13 @@ create, update, and delete interface configurations. ## Operations -| Method | Description | Idempotent | -| ------------------------------------------------------------- | ----------------------------------- | -------------- | -| [`InterfaceList(target)`](list.md) | List network interfaces | Read-only | -| [`InterfaceGet(target, name)`](get.md) | Get a specific interface | Read-only | -| [`InterfaceCreate(target, name, opts)`](create.md) | Create an interface configuration | Non-idempotent | -| [`InterfaceUpdate(target, name, opts)`](update.md) | Update an interface configuration | Idempotent | -| [`InterfaceDelete(target, name)`](delete.md) | Delete an interface configuration | Idempotent | +| Method | Description | Idempotent | +| -------------------------------------------------- | --------------------------------- | -------------- | +| [`InterfaceList(target)`](list.md) | List network interfaces | Read-only | +| [`InterfaceGet(target, name)`](get.md) | Get a specific interface | Read-only | +| [`InterfaceCreate(target, name, opts)`](create.md) | Create an interface configuration | Non-idempotent | +| [`InterfaceUpdate(target, name, opts)`](update.md) | Update an interface configuration | Idempotent | +| [`InterfaceDelete(target, name)`](delete.md) | Delete an interface configuration | Idempotent | ## Permissions @@ -22,6 +22,5 @@ create, update, and delete interface configurations. ## Example -See -[`examples/operations/interface.go`](../../examples/operations/interface.go) for -a complete workflow example covering all operations. +See [`examples/operations/interface.go`](../../examples/operations/interface.go) +for a complete workflow example covering all operations. diff --git a/docs/operations/interface/create.md b/docs/operations/interface/create.md index 13e96a3..b33bbf9 100644 --- a/docs/operations/interface/create.md +++ b/docs/operations/interface/create.md @@ -21,14 +21,14 @@ step := o.InterfaceCreate("web-01", "eth1", osapi.InterfaceConfigOpts{ ### InterfaceConfigOpts -| Field | Type | Required | Description | -| ----------- | ---------- | -------- | ---------------------------------------- | -| `DHCP4` | `*bool` | No | Enable or disable DHCPv4. | -| `DHCP6` | `*bool` | No | Enable or disable DHCPv6. | -| `Addresses` | `[]string` | No | IP addresses in CIDR notation. | -| `Gateway4` | `string` | No | IPv4 gateway address. | -| `Gateway6` | `string` | No | IPv6 gateway address. | -| `MTU` | `*int` | No | Maximum transmission unit. | +| Field | Type | Required | Description | +| ----------- | ---------- | -------- | ------------------------------ | +| `DHCP4` | `*bool` | No | Enable or disable DHCPv4. | +| `DHCP6` | `*bool` | No | Enable or disable DHCPv6. | +| `Addresses` | `[]string` | No | IP addresses in CIDR notation. | +| `Gateway4` | `string` | No | IPv4 gateway address. | +| `Gateway6` | `string` | No | IPv6 gateway address. | +| `MTU` | `*int` | No | Maximum transmission unit. | ## Result Type diff --git a/docs/operations/interface/delete.md b/docs/operations/interface/delete.md index d54d6a7..08c9660 100644 --- a/docs/operations/interface/delete.md +++ b/docs/operations/interface/delete.md @@ -22,11 +22,11 @@ var result osapi.InterfaceMutationResult err := results.Decode("delete-interface-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------------- | -| `Name` | `string` | Interface name. | -| `Changed` | `bool` | Whether the configuration was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------------- | +| `Name` | `string` | Interface name. | +| `Changed` | `bool` | Whether the configuration was removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/log/README.md b/docs/operations/log/README.md index 93232c1..9ba7fae 100644 --- a/docs/operations/log/README.md +++ b/docs/operations/log/README.md @@ -5,17 +5,17 @@ sources, and query by unit name. ## Operations -| Method | Description | Idempotent | -| ---------------------------------------------------- | -------------------------- | ---------- | -| [`LogQuery(target, opts)`](query.md) | Query journal log entries | Read-only | -| [`LogSources(target)`](sources.md) | List available log sources | Read-only | -| [`LogQueryUnit(target, unit, opts)`](query-unit.md) | Query logs for a unit | Read-only | +| Method | Description | Idempotent | +| --------------------------------------------------- | -------------------------- | ---------- | +| [`LogQuery(target, opts)`](query.md) | Query journal log entries | Read-only | +| [`LogSources(target)`](sources.md) | List available log sources | Read-only | +| [`LogQueryUnit(target, unit, opts)`](query-unit.md) | Query logs for a unit | Read-only | ## Permissions -| Operation | Permission | -| --------------- | ---------- | -| All operations | `log:read` | +| Operation | Permission | +| -------------- | ---------- | +| All operations | `log:read` | ## Example diff --git a/docs/operations/log/query-unit.md b/docs/operations/log/query-unit.md index 86d9113..a14526e 100644 --- a/docs/operations/log/query-unit.md +++ b/docs/operations/log/query-unit.md @@ -17,7 +17,7 @@ step := o.LogQueryUnit("web-01", "nginx", osapi.LogQueryOpts{ | --------- | -------------- | --------------------------------------------------------- | | `target` | `string` | Target host: `_any`, `_all`, hostname, or label selector. | | `unit` | `string` | Systemd unit name to query logs for. | -| `opts` | `LogQueryOpts` | Query options (see [LogQuery](query.md)). | +| `opts` | `LogQueryOpts` | Query options (see [LogQuery](query.md)). | ## Result Type diff --git a/docs/operations/log/query.md b/docs/operations/log/query.md index 206f41c..d29c3c2 100644 --- a/docs/operations/log/query.md +++ b/docs/operations/log/query.md @@ -20,11 +20,11 @@ step := o.LogQuery("web-01", osapi.LogQueryOpts{ ### LogQueryOpts -| Field | Type | Required | Description | -| ---------- | --------- | -------- | ------------------------------------------------------ | -| `Lines` | `*int` | No | Maximum number of log lines to return. | -| `Since` | `*string` | No | Filter entries since this time (e.g., `1h`). | -| `Priority` | `*string` | No | Filter by priority level (e.g., `err`, `warning`). | +| Field | Type | Required | Description | +| ---------- | --------- | -------- | -------------------------------------------------- | +| `Lines` | `*int` | No | Maximum number of log lines to return. | +| `Since` | `*string` | No | Filter entries since this time (e.g., `1h`). | +| `Priority` | `*string` | No | Filter by priority level (e.g., `err`, `warning`). | ## Result Type @@ -41,14 +41,14 @@ err := results.Decode("query-log-1", &result) ### LogEntry -| Field | Type | Description | -| ----------- | -------- | ------------------------ | -| `Timestamp` | `string` | Entry timestamp. | -| `Unit` | `string` | Systemd unit name. | -| `Priority` | `string` | Log priority level. | -| `Message` | `string` | Log message. | -| `PID` | `int` | Process ID. | -| `Hostname` | `string` | Source hostname. | +| Field | Type | Description | +| ----------- | -------- | ------------------- | +| `Timestamp` | `string` | Entry timestamp. | +| `Unit` | `string` | Systemd unit name. | +| `Priority` | `string` | Log priority level. | +| `Message` | `string` | Log message. | +| `PID` | `int` | Process ID. | +| `Hostname` | `string` | Source hostname. | ## Idempotency diff --git a/docs/operations/network/dns-delete.md b/docs/operations/network/dns-delete.md index f9b5a8f..528436b 100644 --- a/docs/operations/network/dns-delete.md +++ b/docs/operations/network/dns-delete.md @@ -23,10 +23,10 @@ var result osapi.DNSDeleteResult err := results.Decode("delete-dns-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------- | +| Field | Type | Description | +| --------- | -------- | ----------------------------------------------------- | | `Changed` | `bool` | Whether the DNS config was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/ntp/README.md b/docs/operations/ntp/README.md index 9a6b574..a5765c8 100644 --- a/docs/operations/ntp/README.md +++ b/docs/operations/ntp/README.md @@ -5,12 +5,12 @@ and delete NTP configuration. ## Operations -| Method | Description | Idempotent | -| ----------------------------------------- | ------------------------ | -------------- | -| [`NTPGet(target)`](get.md) | Get NTP status | Read-only | -| [`NTPCreate(target, opts)`](create.md) | Create NTP configuration | Non-idempotent | -| [`NTPUpdate(target, opts)`](update.md) | Update NTP configuration | Idempotent | -| [`NTPDelete(target)`](delete.md) | Delete NTP configuration | Idempotent | +| Method | Description | Idempotent | +| -------------------------------------- | ------------------------ | -------------- | +| [`NTPGet(target)`](get.md) | Get NTP status | Read-only | +| [`NTPCreate(target, opts)`](create.md) | Create NTP configuration | Non-idempotent | +| [`NTPUpdate(target, opts)`](update.md) | Update NTP configuration | Idempotent | +| [`NTPDelete(target)`](delete.md) | Delete NTP configuration | Idempotent | ## Permissions diff --git a/docs/operations/ntp/create.md b/docs/operations/ntp/create.md index 94a8303..be6b417 100644 --- a/docs/operations/ntp/create.md +++ b/docs/operations/ntp/create.md @@ -20,9 +20,9 @@ step := o.NTPCreate("web-01", osapi.NtpCreateOpts{ ### NtpCreateOpts -| Field | Type | Required | Description | -| --------- | ---------- | -------- | ----------------------------------- | -| `Servers` | `[]string` | Yes | NTP server addresses to configure. | +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ---------------------------------- | +| `Servers` | `[]string` | Yes | NTP server addresses to configure. | ## Result Type @@ -38,8 +38,8 @@ err := results.Decode("create-ntp-1", &result) ## Idempotency -**Non-idempotent.** Creating configuration that already exists returns an -error. Use [NTPUpdate](update.md) to modify existing configuration. +**Non-idempotent.** Creating configuration that already exists returns an error. +Use [NTPUpdate](update.md) to modify existing configuration. ## Permissions diff --git a/docs/operations/ntp/delete.md b/docs/operations/ntp/delete.md index 658eec8..8e81c37 100644 --- a/docs/operations/ntp/delete.md +++ b/docs/operations/ntp/delete.md @@ -21,10 +21,10 @@ var result osapi.NtpMutationResult err := results.Decode("delete-ntp-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ----------------------------------------------------- | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------------- | | `Changed` | `bool` | Whether the configuration was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/ntp/update.md b/docs/operations/ntp/update.md index 9553c66..0399918 100644 --- a/docs/operations/ntp/update.md +++ b/docs/operations/ntp/update.md @@ -19,9 +19,9 @@ step := o.NTPUpdate("web-01", osapi.NtpUpdateOpts{ ### NtpUpdateOpts -| Field | Type | Required | Description | -| --------- | ---------- | -------- | ----------------------------------- | -| `Servers` | `[]string` | Yes | NTP server addresses to configure. | +| Field | Type | Required | Description | +| --------- | ---------- | -------- | ---------------------------------- | +| `Servers` | `[]string` | Yes | NTP server addresses to configure. | ## Result Type diff --git a/docs/operations/package/README.md b/docs/operations/package/README.md index b1d9425..2d25e6c 100644 --- a/docs/operations/package/README.md +++ b/docs/operations/package/README.md @@ -5,14 +5,14 @@ install, remove, update all, and check for available updates. ## Operations -| Method | Description | Idempotent | -| --------------------------------------------------- | ------------------------------ | -------------- | -| [`PackageList(target)`](list.md) | List installed packages | Read-only | -| [`PackageGet(target, name)`](get.md) | Get a specific package | Read-only | -| [`PackageInstall(target, name)`](install.md) | Install a package | Idempotent | -| [`PackageRemove(target, name)`](remove.md) | Remove a package | Idempotent | -| [`PackageUpdate(target)`](update.md) | Update all packages | Non-idempotent | -| [`PackageListUpdates(target)`](list-updates.md) | List available package updates | Read-only | +| Method | Description | Idempotent | +| ----------------------------------------------- | ------------------------------ | -------------- | +| [`PackageList(target)`](list.md) | List installed packages | Read-only | +| [`PackageGet(target, name)`](get.md) | Get a specific package | Read-only | +| [`PackageInstall(target, name)`](install.md) | Install a package | Idempotent | +| [`PackageRemove(target, name)`](remove.md) | Remove a package | Idempotent | +| [`PackageUpdate(target)`](update.md) | Update all packages | Non-idempotent | +| [`PackageListUpdates(target)`](list-updates.md) | List available package updates | Read-only | ## Permissions @@ -23,6 +23,5 @@ install, remove, update all, and check for available updates. ## Example -See -[`examples/operations/package.go`](../../examples/operations/package.go) for a -complete workflow example covering all operations. +See [`examples/operations/package.go`](../../examples/operations/package.go) for +a complete workflow example covering all operations. diff --git a/docs/operations/package/install.md b/docs/operations/package/install.md index f703522..e573806 100644 --- a/docs/operations/package/install.md +++ b/docs/operations/package/install.md @@ -22,11 +22,11 @@ var result osapi.PackageMutationResult err := results.Decode("install-package-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------- | -| `Name` | `string` | Name of the package. | -| `Changed` | `bool` | Whether the package was installed. | -| `Error` | `string` | Error message if install failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Name of the package. | +| `Changed` | `bool` | Whether the package was installed. | +| `Error` | `string` | Error message if install failed; empty on success. | ## Idempotency diff --git a/docs/operations/package/list-updates.md b/docs/operations/package/list-updates.md index 9a870fc..5e19bff 100644 --- a/docs/operations/package/list-updates.md +++ b/docs/operations/package/list-updates.md @@ -29,10 +29,10 @@ err := results.Decode("list-package-updates-1", &result) ### UpdateInfo -| Field | Type | Description | -| ---------------- | -------- | ------------------------------ | -| `Name` | `string` | Package name. | -| `CurrentVersion` | `string` | Currently installed version. | +| Field | Type | Description | +| ---------------- | -------- | ---------------------------- | +| `Name` | `string` | Package name. | +| `CurrentVersion` | `string` | Currently installed version. | ## Idempotency diff --git a/docs/operations/package/list.md b/docs/operations/package/list.md index 4f00f9a..587d8b8 100644 --- a/docs/operations/package/list.md +++ b/docs/operations/package/list.md @@ -29,13 +29,13 @@ err := results.Decode("list-package-1", &result) ### PackageInfo -| Field | Type | Description | -| ------------- | -------- | ------------------------- | -| `Name` | `string` | Package name. | -| `Version` | `string` | Installed version. | -| `Description` | `string` | Package description. | -| `Status` | `string` | Installation status. | -| `Size` | `int64` | Installed size in bytes. | +| Field | Type | Description | +| ------------- | -------- | ------------------------ | +| `Name` | `string` | Package name. | +| `Version` | `string` | Installed version. | +| `Description` | `string` | Package description. | +| `Status` | `string` | Installation status. | +| `Size` | `int64` | Installed size in bytes. | ## Idempotency diff --git a/docs/operations/package/remove.md b/docs/operations/package/remove.md index f338b29..5048a37 100644 --- a/docs/operations/package/remove.md +++ b/docs/operations/package/remove.md @@ -22,11 +22,11 @@ var result osapi.PackageMutationResult err := results.Decode("remove-package-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | --------------------------------------------------- | -| `Name` | `string` | Name of the package. | -| `Changed` | `bool` | Whether the package was removed or already absent. | -| `Error` | `string` | Error message if removal failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Name of the package. | +| `Changed` | `bool` | Whether the package was removed or already absent. | +| `Error` | `string` | Error message if removal failed; empty on success. | ## Idempotency diff --git a/docs/operations/package/update.md b/docs/operations/package/update.md index cf3dd5e..e4ecfcb 100644 --- a/docs/operations/package/update.md +++ b/docs/operations/package/update.md @@ -21,10 +21,10 @@ var result osapi.PackageMutationResult err := results.Decode("update-package-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | -------------------------------------------------- | -| `Changed` | `bool` | Whether any packages were updated. | -| `Error` | `string` | Error message if update failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Changed` | `bool` | Whether any packages were updated. | +| `Error` | `string` | Error message if update failed; empty on success. | ## Idempotency diff --git a/docs/operations/power/README.md b/docs/operations/power/README.md index 3fab5cc..6454bcc 100644 --- a/docs/operations/power/README.md +++ b/docs/operations/power/README.md @@ -4,16 +4,16 @@ Reboot or shut down target nodes. ## Operations -| Method | Description | Idempotent | -| -------------------------------------------- | ----------------------- | -------------- | -| [`PowerReboot(target, opts)`](reboot.md) | Reboot the node | Non-idempotent | -| [`PowerShutdown(target, opts)`](shutdown.md) | Shut down the node | Non-idempotent | +| Method | Description | Idempotent | +| -------------------------------------------- | ------------------ | -------------- | +| [`PowerReboot(target, opts)`](reboot.md) | Reboot the node | Non-idempotent | +| [`PowerShutdown(target, opts)`](shutdown.md) | Shut down the node | Non-idempotent | ## Permissions -| Operation | Permission | -| ---------------- | --------------- | -| All operations | `power:execute` | +| Operation | Permission | +| -------------- | --------------- | +| All operations | `power:execute` | ## Example diff --git a/docs/operations/power/reboot.md b/docs/operations/power/reboot.md index 85ef111..f2a27e8 100644 --- a/docs/operations/power/reboot.md +++ b/docs/operations/power/reboot.md @@ -20,10 +20,10 @@ step := o.PowerReboot("web-01", osapi.PowerOpts{ ### PowerOpts -| Field | Type | Required | Description | -| --------- | -------- | -------- | ------------------------------------------------- | -| `Delay` | `int` | No | Seconds to wait before rebooting. | -| `Message` | `string` | No | Message to broadcast before the reboot. | +| Field | Type | Required | Description | +| --------- | -------- | -------- | --------------------------------------- | +| `Delay` | `int` | No | Seconds to wait before rebooting. | +| `Message` | `string` | No | Message to broadcast before the reboot. | ## Result Type @@ -32,12 +32,12 @@ var result osapi.PowerResult err := results.Decode("reboot-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | -------------------------------------------------- | -| `Action` | `string` | Action taken (e.g., `reboot`). | -| `Delay` | `int` | Delay in seconds before execution. | -| `Changed` | `bool` | Whether the reboot was initiated. | -| `Error` | `string` | Error message if reboot failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Action` | `string` | Action taken (e.g., `reboot`). | +| `Delay` | `int` | Delay in seconds before execution. | +| `Changed` | `bool` | Whether the reboot was initiated. | +| `Error` | `string` | Error message if reboot failed; empty on success. | ## Idempotency diff --git a/docs/operations/power/shutdown.md b/docs/operations/power/shutdown.md index bdcd20a..1a4148f 100644 --- a/docs/operations/power/shutdown.md +++ b/docs/operations/power/shutdown.md @@ -20,10 +20,10 @@ step := o.PowerShutdown("web-01", osapi.PowerOpts{ ### PowerOpts -| Field | Type | Required | Description | -| --------- | -------- | -------- | ------------------------------------------------- | -| `Delay` | `int` | No | Seconds to wait before shutting down. | -| `Message` | `string` | No | Message to broadcast before the shutdown. | +| Field | Type | Required | Description | +| --------- | -------- | -------- | ----------------------------------------- | +| `Delay` | `int` | No | Seconds to wait before shutting down. | +| `Message` | `string` | No | Message to broadcast before the shutdown. | ## Result Type @@ -32,12 +32,12 @@ var result osapi.PowerResult err := results.Decode("shutdown-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------- | -| `Action` | `string` | Action taken (e.g., `shutdown`). | -| `Delay` | `int` | Delay in seconds before execution. | -| `Changed` | `bool` | Whether the shutdown was initiated. | -| `Error` | `string` | Error message if shutdown failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | --------------------------------------------------- | +| `Action` | `string` | Action taken (e.g., `shutdown`). | +| `Delay` | `int` | Delay in seconds before execution. | +| `Changed` | `bool` | Whether the shutdown was initiated. | +| `Error` | `string` | Error message if shutdown failed; empty on success. | ## Idempotency diff --git a/docs/operations/process/README.md b/docs/operations/process/README.md index 46edbcf..874adaa 100644 --- a/docs/operations/process/README.md +++ b/docs/operations/process/README.md @@ -4,11 +4,11 @@ List, inspect, and signal running processes on target nodes. ## Operations -| Method | Description | Idempotent | -| -------------------------------------------------- | ---------------------------- | -------------- | -| [`ProcessList(target)`](list.md) | List running processes | Read-only | -| [`ProcessGet(target, pid)`](get.md) | Get a specific process | Read-only | -| [`ProcessSignal(target, pid, opts)`](signal.md) | Send a signal to a process | Non-idempotent | +| Method | Description | Idempotent | +| ----------------------------------------------- | -------------------------- | -------------- | +| [`ProcessList(target)`](list.md) | List running processes | Read-only | +| [`ProcessGet(target, pid)`](get.md) | Get a specific process | Read-only | +| [`ProcessSignal(target, pid, opts)`](signal.md) | Send a signal to a process | Non-idempotent | ## Permissions @@ -19,6 +19,5 @@ List, inspect, and signal running processes on target nodes. ## Example -See -[`examples/operations/process.go`](../../examples/operations/process.go) for a -complete workflow example covering all operations. +See [`examples/operations/process.go`](../../examples/operations/process.go) for +a complete workflow example covering all operations. diff --git a/docs/operations/process/signal.md b/docs/operations/process/signal.md index 74b3b28..0927c18 100644 --- a/docs/operations/process/signal.md +++ b/docs/operations/process/signal.md @@ -20,9 +20,9 @@ step := o.ProcessSignal("web-01", 1234, osapi.ProcessSignalOpts{ ### ProcessSignalOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | ------------------------------------------------ | -| `Signal` | `string` | Yes | Signal name (e.g., `TERM`, `KILL`, `HUP`). | +| Field | Type | Required | Description | +| -------- | -------- | -------- | ------------------------------------------ | +| `Signal` | `string` | Yes | Signal name (e.g., `TERM`, `KILL`, `HUP`). | ## Result Type @@ -31,12 +31,12 @@ var result osapi.ProcessSignalResult err := results.Decode("signal-process-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | -------------------------------------------------- | -| `PID` | `int` | Process ID that was signaled. | -| `Signal` | `string` | Signal that was sent. | -| `Changed` | `bool` | Whether the signal was delivered. | -| `Error` | `string` | Error message if signal failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `PID` | `int` | Process ID that was signaled. | +| `Signal` | `string` | Signal that was sent. | +| `Changed` | `bool` | Whether the signal was delivered. | +| `Error` | `string` | Error message if signal failed; empty on success. | ## Idempotency diff --git a/docs/operations/route/README.md b/docs/operations/route/README.md index c8806e6..75cb952 100644 --- a/docs/operations/route/README.md +++ b/docs/operations/route/README.md @@ -5,13 +5,13 @@ create, update, and delete routes. ## Operations -| Method | Description | Idempotent | -| ------------------------------------------------------------ | --------------------------- | -------------- | -| [`RouteList(target)`](list.md) | List network routes | Read-only | -| [`RouteGet(target, iface)`](get.md) | Get routes for an interface | Read-only | -| [`RouteCreate(target, iface, opts)`](create.md) | Create route configuration | Non-idempotent | -| [`RouteUpdate(target, iface, opts)`](update.md) | Update route configuration | Idempotent | -| [`RouteDelete(target, iface)`](delete.md) | Delete route configuration | Idempotent | +| Method | Description | Idempotent | +| ----------------------------------------------- | --------------------------- | -------------- | +| [`RouteList(target)`](list.md) | List network routes | Read-only | +| [`RouteGet(target, iface)`](get.md) | Get routes for an interface | Read-only | +| [`RouteCreate(target, iface, opts)`](create.md) | Create route configuration | Non-idempotent | +| [`RouteUpdate(target, iface, opts)`](update.md) | Update route configuration | Idempotent | +| [`RouteDelete(target, iface)`](delete.md) | Delete route configuration | Idempotent | ## Permissions diff --git a/docs/operations/route/create.md b/docs/operations/route/create.md index 606969d..d985adf 100644 --- a/docs/operations/route/create.md +++ b/docs/operations/route/create.md @@ -22,16 +22,16 @@ step := o.RouteCreate("web-01", "eth0", osapi.RouteConfigOpts{ ### RouteConfigOpts -| Field | Type | Required | Description | -| -------- | ------------- | -------- | --------------------- | +| Field | Type | Required | Description | +| -------- | ------------- | -------- | ---------------------- | | `Routes` | `[]RouteItem` | Yes | List of route entries. | ### RouteItem -| Field | Type | Required | Description | -| ----- | -------- | -------- | ----------------------------------- | -| `To` | `string` | Yes | Destination in CIDR notation. | -| `Via` | `string` | Yes | Gateway IP address. | +| Field | Type | Required | Description | +| ----- | -------- | -------- | ----------------------------- | +| `To` | `string` | Yes | Destination in CIDR notation. | +| `Via` | `string` | Yes | Gateway IP address. | ## Result Type diff --git a/docs/operations/route/delete.md b/docs/operations/route/delete.md index cccc75b..453ab2d 100644 --- a/docs/operations/route/delete.md +++ b/docs/operations/route/delete.md @@ -22,11 +22,11 @@ var result osapi.RouteMutationResult err := results.Decode("delete-route-1", &result) ``` -| Field | Type | Description | -| ----------- | -------- | ------------------------------------------------------ | -| `Interface` | `string` | Interface name. | -| `Changed` | `bool` | Whether the routes were removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| Field | Type | Description | +| ----------- | -------- | --------------------------------------------------- | +| `Interface` | `string` | Interface name. | +| `Changed` | `bool` | Whether the routes were removed or already absent. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/README.md b/docs/operations/service/README.md index 5550812..df8376f 100644 --- a/docs/operations/service/README.md +++ b/docs/operations/service/README.md @@ -5,18 +5,18 @@ files, and control service lifecycle (start, stop, restart, enable, disable). ## Operations -| Method | Description | Idempotent | -| ------------------------------------------------------------ | --------------------------- | -------------- | -| [`ServiceList(target)`](list.md) | List services | Read-only | -| [`ServiceGet(target, name)`](get.md) | Get a specific service | Read-only | -| [`ServiceCreate(target, opts)`](create.md) | Create a service unit file | Non-idempotent | -| [`ServiceUpdate(target, name, opts)`](update.md) | Update a service unit file | Idempotent | -| [`ServiceDelete(target, name)`](delete.md) | Delete a service unit file | Idempotent | -| [`ServiceStart(target, name)`](start.md) | Start a service | Idempotent | -| [`ServiceStop(target, name)`](stop.md) | Stop a service | Idempotent | -| [`ServiceRestart(target, name)`](restart.md) | Restart a service | Non-idempotent | -| [`ServiceEnable(target, name)`](enable.md) | Enable a service on boot | Idempotent | -| [`ServiceDisable(target, name)`](disable.md) | Disable a service on boot | Idempotent | +| Method | Description | Idempotent | +| ------------------------------------------------ | -------------------------- | -------------- | +| [`ServiceList(target)`](list.md) | List services | Read-only | +| [`ServiceGet(target, name)`](get.md) | Get a specific service | Read-only | +| [`ServiceCreate(target, opts)`](create.md) | Create a service unit file | Non-idempotent | +| [`ServiceUpdate(target, name, opts)`](update.md) | Update a service unit file | Idempotent | +| [`ServiceDelete(target, name)`](delete.md) | Delete a service unit file | Idempotent | +| [`ServiceStart(target, name)`](start.md) | Start a service | Idempotent | +| [`ServiceStop(target, name)`](stop.md) | Stop a service | Idempotent | +| [`ServiceRestart(target, name)`](restart.md) | Restart a service | Non-idempotent | +| [`ServiceEnable(target, name)`](enable.md) | Enable a service on boot | Idempotent | +| [`ServiceDisable(target, name)`](disable.md) | Disable a service on boot | Idempotent | ## Permissions @@ -27,6 +27,5 @@ files, and control service lifecycle (start, stop, restart, enable, disable). ## Example -See -[`examples/operations/service.go`](../../examples/operations/service.go) for a -complete workflow example covering all operations. +See [`examples/operations/service.go`](../../examples/operations/service.go) for +a complete workflow example covering all operations. diff --git a/docs/operations/service/create.md b/docs/operations/service/create.md index 50796af..554fd3c 100644 --- a/docs/operations/service/create.md +++ b/docs/operations/service/create.md @@ -21,10 +21,10 @@ step := o.ServiceCreate("web-01", osapi.ServiceCreateOpts{ ### ServiceCreateOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | ---------------------------------------------- | -| `Name` | `string` | Yes | Service unit name. | -| `Object` | `string` | Yes | Object Store reference for the unit file. | +| Field | Type | Required | Description | +| -------- | -------- | -------- | ----------------------------------------- | +| `Name` | `string` | Yes | Service unit name. | +| `Object` | `string` | Yes | Object Store reference for the unit file. | ## Result Type @@ -41,8 +41,8 @@ err := results.Decode("create-service-1", &result) ## Idempotency -**Non-idempotent.** Creating a service that already exists returns an error. -Use [ServiceUpdate](update.md) to modify existing unit files. +**Non-idempotent.** Creating a service that already exists returns an error. Use +[ServiceUpdate](update.md) to modify existing unit files. ## Permissions diff --git a/docs/operations/service/disable.md b/docs/operations/service/disable.md index 107a809..e6c3f5d 100644 --- a/docs/operations/service/disable.md +++ b/docs/operations/service/disable.md @@ -22,11 +22,11 @@ var result osapi.ServiceMutationResult err := results.Decode("disable-service-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | --------------------------------------------------- | -| `Name` | `string` | Name of the disabled service. | -| `Changed` | `bool` | Whether the service was disabled. | -| `Error` | `string` | Error message if disable failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Name of the disabled service. | +| `Changed` | `bool` | Whether the service was disabled. | +| `Error` | `string` | Error message if disable failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/enable.md b/docs/operations/service/enable.md index d464f98..3133ec4 100644 --- a/docs/operations/service/enable.md +++ b/docs/operations/service/enable.md @@ -22,11 +22,11 @@ var result osapi.ServiceMutationResult err := results.Decode("enable-service-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | -------------------------------------------------- | -| `Name` | `string` | Name of the enabled service. | -| `Changed` | `bool` | Whether the service was enabled. | -| `Error` | `string` | Error message if enable failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------- | +| `Name` | `string` | Name of the enabled service. | +| `Changed` | `bool` | Whether the service was enabled. | +| `Error` | `string` | Error message if enable failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/list.md b/docs/operations/service/list.md index 87f7ed2..df65adb 100644 --- a/docs/operations/service/list.md +++ b/docs/operations/service/list.md @@ -29,13 +29,13 @@ err := results.Decode("list-service-1", &result) ### ServiceInfo -| Field | Type | Description | -| ------------- | -------- | ------------------------------------ | -| `Name` | `string` | Service unit name. | -| `Status` | `string` | Active status of the service. | -| `Enabled` | `bool` | Whether the service starts on boot. | -| `Description` | `string` | Service description from unit file. | -| `PID` | `int` | Main process ID of the service. | +| Field | Type | Description | +| ------------- | -------- | ----------------------------------- | +| `Name` | `string` | Service unit name. | +| `Status` | `string` | Active status of the service. | +| `Enabled` | `bool` | Whether the service starts on boot. | +| `Description` | `string` | Service description from unit file. | +| `PID` | `int` | Main process ID of the service. | ## Idempotency diff --git a/docs/operations/service/restart.md b/docs/operations/service/restart.md index 090cdc2..948c86c 100644 --- a/docs/operations/service/restart.md +++ b/docs/operations/service/restart.md @@ -22,11 +22,11 @@ var result osapi.ServiceMutationResult err := results.Decode("restart-service-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | --------------------------------------------------- | -| `Name` | `string` | Name of the restarted service. | -| `Changed` | `bool` | Whether the service was restarted. | -| `Error` | `string` | Error message if restart failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Name of the restarted service. | +| `Changed` | `bool` | Whether the service was restarted. | +| `Error` | `string` | Error message if restart failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/start.md b/docs/operations/service/start.md index 2ab23d9..0b2e7dc 100644 --- a/docs/operations/service/start.md +++ b/docs/operations/service/start.md @@ -22,11 +22,11 @@ var result osapi.ServiceMutationResult err := results.Decode("start-service-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ------------------------------------------------- | -| `Name` | `string` | Name of the started service. | -| `Changed` | `bool` | Whether the service was started. | -| `Error` | `string` | Error message if start failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ------------------------------------------------ | +| `Name` | `string` | Name of the started service. | +| `Changed` | `bool` | Whether the service was started. | +| `Error` | `string` | Error message if start failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/stop.md b/docs/operations/service/stop.md index d2cc492..5b8fba4 100644 --- a/docs/operations/service/stop.md +++ b/docs/operations/service/stop.md @@ -22,11 +22,11 @@ var result osapi.ServiceMutationResult err := results.Decode("stop-service-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ------------------------------------------------ | -| `Name` | `string` | Name of the stopped service. | -| `Changed` | `bool` | Whether the service was stopped. | -| `Error` | `string` | Error message if stop failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ----------------------------------------------- | +| `Name` | `string` | Name of the stopped service. | +| `Changed` | `bool` | Whether the service was stopped. | +| `Error` | `string` | Error message if stop failed; empty on success. | ## Idempotency diff --git a/docs/operations/service/update.md b/docs/operations/service/update.md index b9644e0..5f513eb 100644 --- a/docs/operations/service/update.md +++ b/docs/operations/service/update.md @@ -20,9 +20,9 @@ step := o.ServiceUpdate("web-01", "myapp", osapi.ServiceUpdateOpts{ ### ServiceUpdateOpts -| Field | Type | Required | Description | -| -------- | -------- | -------- | ---------------------------------------------- | -| `Object` | `string` | Yes | New Object Store reference for the unit file. | +| Field | Type | Required | Description | +| -------- | -------- | -------- | --------------------------------------------- | +| `Object` | `string` | Yes | New Object Store reference for the unit file. | ## Result Type diff --git a/docs/operations/sysctl/README.md b/docs/operations/sysctl/README.md index 6041b73..72c9932 100644 --- a/docs/operations/sysctl/README.md +++ b/docs/operations/sysctl/README.md @@ -22,6 +22,5 @@ update, list, get, and delete parameters backed by the file provider. ## Example -See -[`examples/operations/sysctl.go`](../../examples/operations/sysctl.go) for a +See [`examples/operations/sysctl.go`](../../examples/operations/sysctl.go) for a complete workflow example covering all operations. diff --git a/docs/operations/sysctl/create.md b/docs/operations/sysctl/create.md index 06daad7..1110552 100644 --- a/docs/operations/sysctl/create.md +++ b/docs/operations/sysctl/create.md @@ -20,10 +20,10 @@ step := o.SysctlCreate("web-01", osapi.SysctlCreateOpts{ ### SysctlCreateOpts -| Field | Type | Required | Description | -| ------- | -------- | -------- | ------------------------------------------------- | +| Field | Type | Required | Description | +| ------- | -------- | -------- | --------------------------------------------------- | | `Key` | `string` | Yes | Sysctl parameter key (e.g., `net.ipv4.ip_forward`). | -| `Value` | `string` | Yes | Value to set for the parameter. | +| `Value` | `string` | Yes | Value to set for the parameter. | ## Result Type diff --git a/docs/operations/sysctl/delete.md b/docs/operations/sysctl/delete.md index 1e09eff..661dcc3 100644 --- a/docs/operations/sysctl/delete.md +++ b/docs/operations/sysctl/delete.md @@ -22,11 +22,11 @@ var result osapi.SysctlMutationResult err := results.Decode("delete-sysctl-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | --------------------------------------------------- | -| `Key` | `string` | Sysctl parameter key. | +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------------- | +| `Key` | `string` | Sysctl parameter key. | | `Changed` | `bool` | Whether the parameter was removed or already absent. | -| `Error` | `string` | Error message if deletion failed; empty on success. | +| `Error` | `string` | Error message if deletion failed; empty on success. | ## Idempotency diff --git a/docs/operations/sysctl/update.md b/docs/operations/sysctl/update.md index 7154433..ec51ae2 100644 --- a/docs/operations/sysctl/update.md +++ b/docs/operations/sysctl/update.md @@ -20,8 +20,8 @@ step := o.SysctlUpdate("web-01", "net.ipv4.ip_forward", osapi.SysctlUpdateOpts{ ### SysctlUpdateOpts -| Field | Type | Required | Description | -| ------- | -------- | -------- | ------------------------- | +| Field | Type | Required | Description | +| ------- | -------- | -------- | ---------------------------- | | `Value` | `string` | Yes | New value for the parameter. | ## Result Type diff --git a/docs/operations/timezone/README.md b/docs/operations/timezone/README.md index 4ae944e..11a0ed7 100644 --- a/docs/operations/timezone/README.md +++ b/docs/operations/timezone/README.md @@ -5,10 +5,10 @@ update it. ## Operations -| Method | Description | Idempotent | -| ----------------------------------------------- | ------------------------ | ---------- | -| [`TimezoneGet(target)`](get.md) | Get the system timezone | Read-only | -| [`TimezoneUpdate(target, opts)`](update.md) | Set the system timezone | Idempotent | +| Method | Description | Idempotent | +| ------------------------------------------- | ----------------------- | ---------- | +| [`TimezoneGet(target)`](get.md) | Get the system timezone | Read-only | +| [`TimezoneUpdate(target, opts)`](update.md) | Set the system timezone | Idempotent | ## Permissions @@ -19,6 +19,5 @@ update it. ## Example -See -[`examples/operations/timezone.go`](../../examples/operations/timezone.go) for -a complete workflow example. +See [`examples/operations/timezone.go`](../../examples/operations/timezone.go) +for a complete workflow example. diff --git a/docs/operations/timezone/update.md b/docs/operations/timezone/update.md index 2aea85e..9bdea76 100644 --- a/docs/operations/timezone/update.md +++ b/docs/operations/timezone/update.md @@ -19,8 +19,8 @@ step := o.TimezoneUpdate("web-01", osapi.TimezoneUpdateOpts{ ### TimezoneUpdateOpts -| Field | Type | Required | Description | -| ---------- | -------- | -------- | -------------------------------------------------- | +| Field | Type | Required | Description | +| ---------- | -------- | -------- | ----------------------------------------------------- | | `Timezone` | `string` | Yes | IANA timezone name (e.g., `America/New_York`, `UTC`). | ## Result Type @@ -38,8 +38,8 @@ err := results.Decode("update-timezone-1", &result) ## Idempotency -**Idempotent.** Compares the current timezone against the desired value. -Returns `Changed: true` only if the timezone was actually modified. +**Idempotent.** Compares the current timezone against the desired value. Returns +`Changed: true` only if the timezone was actually modified. ## Permissions diff --git a/docs/operations/user/README.md b/docs/operations/user/README.md index ba6d86e..297fbda 100644 --- a/docs/operations/user/README.md +++ b/docs/operations/user/README.md @@ -5,17 +5,17 @@ create, update, delete users, and manage their SSH keys and passwords. ## Operations -| Method | Description | Idempotent | -| ------------------------------------------------------------------------- | ---------------------------- | -------------- | -| [`UserList(target)`](list.md) | List user accounts | Read-only | -| [`UserGet(target, username)`](get.md) | Get a specific user | Read-only | -| [`UserCreate(target, opts)`](create.md) | Create a user account | Non-idempotent | -| [`UserUpdate(target, username, opts)`](update.md) | Update a user account | Idempotent | -| [`UserDelete(target, username)`](delete.md) | Delete a user account | Idempotent | -| [`UserListKeys(target, username)`](list-keys.md) | List SSH authorized keys | Read-only | -| [`UserAddKey(target, username, opts)`](add-key.md) | Add an SSH authorized key | Non-idempotent | -| [`UserRemoveKey(target, username, fingerprint)`](remove-key.md) | Remove an SSH authorized key | Idempotent | -| [`UserChangePassword(target, username, password)`](change-password.md) | Change a user's password | Non-idempotent | +| Method | Description | Idempotent | +| ---------------------------------------------------------------------- | ---------------------------- | -------------- | +| [`UserList(target)`](list.md) | List user accounts | Read-only | +| [`UserGet(target, username)`](get.md) | Get a specific user | Read-only | +| [`UserCreate(target, opts)`](create.md) | Create a user account | Non-idempotent | +| [`UserUpdate(target, username, opts)`](update.md) | Update a user account | Idempotent | +| [`UserDelete(target, username)`](delete.md) | Delete a user account | Idempotent | +| [`UserListKeys(target, username)`](list-keys.md) | List SSH authorized keys | Read-only | +| [`UserAddKey(target, username, opts)`](add-key.md) | Add an SSH authorized key | Non-idempotent | +| [`UserRemoveKey(target, username, fingerprint)`](remove-key.md) | Remove an SSH authorized key | Idempotent | +| [`UserChangePassword(target, username, password)`](change-password.md) | Change a user's password | Non-idempotent | ## Permissions diff --git a/docs/operations/user/add-key.md b/docs/operations/user/add-key.md index 3daa6eb..8f8bcc5 100644 --- a/docs/operations/user/add-key.md +++ b/docs/operations/user/add-key.md @@ -20,9 +20,9 @@ step := o.UserAddKey("web-01", "deploy", osapi.SSHKeyAddOpts{ ### SSHKeyAddOpts -| Field | Type | Required | Description | -| ----- | -------- | -------- | -------------------------------------------- | -| `Key` | `string` | Yes | Full SSH public key line. | +| Field | Type | Required | Description | +| ----- | -------- | -------- | ------------------------- | +| `Key` | `string` | Yes | Full SSH public key line. | ## Result Type @@ -31,10 +31,10 @@ var result osapi.SSHKeyMutationResult err := results.Decode("add-ssh-key-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ------------------------------------------------- | -| `Changed` | `bool` | Whether the key was added. | -| `Error` | `string` | Error message if add failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | ---------------------------------------------- | +| `Changed` | `bool` | Whether the key was added. | +| `Error` | `string` | Error message if add failed; empty on success. | ## Idempotency diff --git a/docs/operations/user/create.md b/docs/operations/user/create.md index 60f61bb..ec6c15d 100644 --- a/docs/operations/user/create.md +++ b/docs/operations/user/create.md @@ -21,13 +21,13 @@ step := o.UserCreate("web-01", osapi.UserCreateOpts{ ### UserCreateOpts -| Field | Type | Required | Description | -| ------- | -------- | -------- | --------------------------------------------------- | -| `Name` | `string` | Yes | Username. | -| `UID` | `int` | No | Numeric user ID (system assigns if zero). | -| `GID` | `int` | No | Primary group ID (creates matching group if zero). | -| `Home` | `string` | No | Home directory path. | -| `Shell` | `string` | No | Login shell path. | +| Field | Type | Required | Description | +| ------- | -------- | -------- | -------------------------------------------------- | +| `Name` | `string` | Yes | Username. | +| `UID` | `int` | No | Numeric user ID (system assigns if zero). | +| `GID` | `int` | No | Primary group ID (creates matching group if zero). | +| `Home` | `string` | No | Home directory path. | +| `Shell` | `string` | No | Login shell path. | ## Result Type diff --git a/docs/operations/user/list-keys.md b/docs/operations/user/list-keys.md index ce40cb7..c8631a2 100644 --- a/docs/operations/user/list-keys.md +++ b/docs/operations/user/list-keys.md @@ -30,11 +30,11 @@ err := results.Decode("list-ssh-key-1", &result) ### SSHKeyInfo -| Field | Type | Description | -| ------------- | -------- | ---------------------- | +| Field | Type | Description | +| ------------- | -------- | ------------------------------- | | `Type` | `string` | Key type (e.g., `ssh-ed25519`). | -| `Fingerprint` | `string` | Key fingerprint. | -| `Comment` | `string` | Key comment. | +| `Fingerprint` | `string` | Key fingerprint. | +| `Comment` | `string` | Key comment. | ## Idempotency diff --git a/docs/operations/user/list.md b/docs/operations/user/list.md index 06027b4..c0beb15 100644 --- a/docs/operations/user/list.md +++ b/docs/operations/user/list.md @@ -29,14 +29,14 @@ err := results.Decode("list-user-1", &result) ### UserInfo -| Field | Type | Description | -| -------- | ---------- | -------------------------- | -| `Name` | `string` | Username. | -| `UID` | `int` | Numeric user ID. | -| `GID` | `int` | Primary group ID. | -| `Home` | `string` | Home directory path. | -| `Shell` | `string` | Login shell path. | -| `Groups` | `[]string` | Supplementary group names. | +| Field | Type | Description | +| -------- | ---------- | ------------------------------ | +| `Name` | `string` | Username. | +| `UID` | `int` | Numeric user ID. | +| `GID` | `int` | Primary group ID. | +| `Home` | `string` | Home directory path. | +| `Shell` | `string` | Login shell path. | +| `Groups` | `[]string` | Supplementary group names. | | `Locked` | `bool` | Whether the account is locked. | ## Idempotency diff --git a/docs/operations/user/remove-key.md b/docs/operations/user/remove-key.md index 53ad9e1..861f8a4 100644 --- a/docs/operations/user/remove-key.md +++ b/docs/operations/user/remove-key.md @@ -23,10 +23,10 @@ var result osapi.SSHKeyMutationResult err := results.Decode("remove-ssh-key-1", &result) ``` -| Field | Type | Description | -| --------- | -------- | ---------------------------------------------------- | -| `Changed` | `bool` | Whether the key was removed or already absent. | -| `Error` | `string` | Error message if removal failed; empty on success. | +| Field | Type | Description | +| --------- | -------- | -------------------------------------------------- | +| `Changed` | `bool` | Whether the key was removed or already absent. | +| `Error` | `string` | Error message if removal failed; empty on success. | ## Idempotency diff --git a/docs/operations/user/update.md b/docs/operations/user/update.md index 6385f02..098990d 100644 --- a/docs/operations/user/update.md +++ b/docs/operations/user/update.md @@ -21,12 +21,12 @@ step := o.UserUpdate("web-01", "deploy", osapi.UserUpdateOpts{ ### UserUpdateOpts -| Field | Type | Required | Description | -| -------- | ---------- | -------- | ------------------------------------------------- | -| `Shell` | `string` | No | New login shell path. | -| `Home` | `string` | No | New home directory path. | -| `Groups` | `[]string` | No | Supplementary group names (replaces existing). | -| `Lock` | `*bool` | No | Lock or unlock the account. | +| Field | Type | Required | Description | +| -------- | ---------- | -------- | ---------------------------------------------- | +| `Shell` | `string` | No | New login shell path. | +| `Home` | `string` | No | New home directory path. | +| `Groups` | `[]string` | No | Supplementary group names (replaces existing). | +| `Lock` | `*bool` | No | Lock or unlock the account. | ## Result Type diff --git a/pkg/orchestrator/ops.go b/pkg/orchestrator/ops.go index c83d3ca..a070b2f 100644 --- a/pkg/orchestrator/ops.go +++ b/pkg/orchestrator/ops.go @@ -772,10 +772,10 @@ func (o *Orchestrator) RouteCreate( return engine.CollectionResult(resp.Data, resp.RawJSON(), func(r osapi.RouteMutationResult) engine.HostResult { return engine.HostResult{ - Hostname: r.Hostname, - Status: r.Status, - Changed: r.Changed, - Error: r.Error, + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, } }, ) @@ -807,10 +807,10 @@ func (o *Orchestrator) RouteUpdate( return engine.CollectionResult(resp.Data, resp.RawJSON(), func(r osapi.RouteMutationResult) engine.HostResult { return engine.HostResult{ - Hostname: r.Hostname, - Status: r.Status, - Changed: r.Changed, - Error: r.Error, + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, } }, ) @@ -841,10 +841,10 @@ func (o *Orchestrator) RouteDelete( return engine.CollectionResult(resp.Data, resp.RawJSON(), func(r osapi.RouteMutationResult) engine.HostResult { return engine.HostResult{ - Hostname: r.Hostname, - Status: r.Status, - Changed: r.Changed, - Error: r.Error, + Hostname: r.Hostname, + Status: r.Status, + Changed: r.Changed, + Error: r.Error, } }, ) diff --git a/pkg/orchestrator/ops_public_test.go b/pkg/orchestrator/ops_public_test.go index 4995401..33b3fa0 100644 --- a/pkg/orchestrator/ops_public_test.go +++ b/pkg/orchestrator/ops_public_test.go @@ -884,6 +884,1350 @@ func (s *OpsPublicTestSuite) TestCronDelete() { } } +func (s *OpsPublicTestSuite) TestNetworkDNSDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.NetworkDNSDelete("_any", "eth0") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestInterfaceList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.InterfaceList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestInterfaceGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.InterfaceGet("_any", "eth0") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestInterfaceCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.InterfaceCreate("_any", "eth0", osapi.InterfaceConfigOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestInterfaceUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.InterfaceUpdate("_any", "eth0", osapi.InterfaceConfigOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestInterfaceDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.InterfaceDelete("_any", "eth0") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestRouteList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.RouteList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestRouteGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.RouteGet("_any", "eth0") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestRouteCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.RouteCreate("_any", "eth0", osapi.RouteConfigOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestRouteUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.RouteUpdate("_any", "eth0", osapi.RouteConfigOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestRouteDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.RouteDelete("_any", "eth0") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestSysctlList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.SysctlList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestSysctlGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.SysctlGet("_any", "net.ipv4.ip_forward") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestSysctlCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.SysctlCreate("_any", osapi.SysctlCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestSysctlUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.SysctlUpdate("_any", "net.ipv4.ip_forward", osapi.SysctlUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestSysctlDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.SysctlDelete("_any", "net.ipv4.ip_forward") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestNTPGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.NTPGet("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestNTPCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.NTPCreate("_any", osapi.NtpCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestNTPUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.NTPUpdate("_any", osapi.NtpUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestNTPDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.NTPDelete("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestTimezoneGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.TimezoneGet("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestTimezoneUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.TimezoneUpdate("_any", osapi.TimezoneUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceGet("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceCreate("_any", osapi.ServiceCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceUpdate("_any", "nginx", osapi.ServiceUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceDelete("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceStart() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceStart("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceStop() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceStop("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceRestart() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceRestart("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceEnable() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceEnable("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestServiceDisable() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ServiceDisable("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageGet("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageInstall() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageInstall("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageRemove() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageRemove("_any", "nginx") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageUpdate("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPackageListUpdates() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PackageListUpdates("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserGet("_any", "admin") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserCreate("_any", osapi.UserCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserUpdate("_any", "admin", osapi.UserUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserDelete("_any", "admin") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserListKeys() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserListKeys("_any", "admin") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserAddKey() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserAddKey("_any", "admin", osapi.SSHKeyAddOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserRemoveKey() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserRemoveKey("_any", "admin", "SHA256:abc123") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestUserChangePassword() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.UserChangePassword("_any", "admin", "newpass") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestGroupList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.GroupList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestGroupGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.GroupGet("_any", "admins") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestGroupCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.GroupCreate("_any", osapi.GroupCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestGroupUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.GroupUpdate("_any", "admins", osapi.GroupUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestGroupDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.GroupDelete("_any", "admins") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestCertificateList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.CertificateList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestCertificateCreate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.CertificateCreate("_any", osapi.CertificateCreateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestCertificateUpdate() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.CertificateUpdate("_any", "my-ca", osapi.CertificateUpdateOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestCertificateDelete() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.CertificateDelete("_any", "my-ca") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestProcessList() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ProcessList("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestProcessGet() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ProcessGet("_any", 1234) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestProcessSignal() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.ProcessSignal("_any", 1234, osapi.ProcessSignalOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPowerReboot() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PowerReboot("_any", osapi.PowerOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestPowerShutdown() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.PowerShutdown("_any", osapi.PowerOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestLogQuery() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.LogQuery("_any", osapi.LogQueryOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestLogSources() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.LogSources("_any") + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + +func (s *OpsPublicTestSuite) TestLogQueryUnit() { + tests := []struct { + name string + newFn func() *orchestrator.Step + }{ + { + name: "Returns non-nil step", + newFn: func() *orchestrator.Step { + return s.orch.LogQueryUnit("_any", "nginx.service", osapi.LogQueryOpts{}) + }, + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + step := tc.newFn() + s.NotNil(step) + }) + } +} + func TestOpsPublicTestSuite( t *testing.T, ) { diff --git a/pkg/orchestrator/ops_test.go b/pkg/orchestrator/ops_test.go index 8c87554..7239b42 100644 --- a/pkg/orchestrator/ops_test.go +++ b/pkg/orchestrator/ops_test.go @@ -3890,6 +3890,4230 @@ func (s *OpsTestSuite) TestCronDeleteNameCounter() { } } +func (s *OpsTestSuite) TestNetworkDNSDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with dns delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete dns", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.NetworkDNSDelete("_any", "eth0") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestInterfaceList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with interface list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list interfaces", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.InterfaceList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestInterfaceGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with interface data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get interface", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.InterfaceGet("_any", "eth0") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestInterfaceCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with interface create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create interface", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.InterfaceCreate("_any", "eth0", osapi.InterfaceConfigOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestInterfaceUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with interface update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update interface", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.InterfaceUpdate("_any", "eth0", osapi.InterfaceConfigOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestInterfaceDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with interface delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete interface", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.InterfaceDelete("_any", "eth0") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestRouteList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with route list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list routes", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.RouteList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestRouteGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with route data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get route", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.RouteGet("_any", "eth0") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestRouteCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with route create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create route", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.RouteCreate("_any", "eth0", osapi.RouteConfigOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestRouteUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with route update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update route", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.RouteUpdate("_any", "eth0", osapi.RouteConfigOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestRouteDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with route delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete route", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.RouteDelete("_any", "eth0") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestSysctlList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with sysctl list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list sysctl", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.SysctlList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestSysctlGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with sysctl data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get sysctl", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.SysctlGet("_any", "net.ipv4.ip_forward") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestSysctlCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with sysctl create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create sysctl", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.SysctlCreate("_any", osapi.SysctlCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestSysctlUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with sysctl update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update sysctl", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.SysctlUpdate("_any", "net.ipv4.ip_forward", osapi.SysctlUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestSysctlDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with sysctl delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete sysctl", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.SysctlDelete("_any", "net.ipv4.ip_forward") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestNTPGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with ntp data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get ntp", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.NTPGet("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestNTPCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with ntp create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create ntp", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.NTPCreate("_any", osapi.NtpCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestNTPUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with ntp update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update ntp", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.NTPUpdate("_any", osapi.NtpUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestNTPDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with ntp delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete ntp", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.NTPDelete("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestTimezoneGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with timezone data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get timezone", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.TimezoneGet("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestTimezoneUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with timezone update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update timezone", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.TimezoneUpdate("_any", osapi.TimezoneUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list services", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceGet("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceCreate("_any", osapi.ServiceCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceUpdate("_any", "nginx", osapi.ServiceUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceDelete("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceStart() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service start data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "start service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceStart("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceStop() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service stop data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "stop service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceStop("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceRestart() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service restart data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "restart service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceRestart("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceEnable() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service enable data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "enable service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceEnable("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestServiceDisable() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with service disable data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "disable service", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ServiceDisable("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list packages", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get package", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageGet("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageInstall() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package install data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "install package", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageInstall("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageRemove() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package remove data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "remove package", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageRemove("_any", "nginx") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update packages", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageUpdate("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPackageListUpdates() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with package updates data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list package updates", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PackageListUpdates("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with user list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list users", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with user data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get user", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserGet("_any", "admin") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with user create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create user", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserCreate("_any", osapi.UserCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with user update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update user", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserUpdate("_any", "admin", osapi.UserUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with user delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete user", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserDelete("_any", "admin") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserListKeys() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with ssh keys data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list ssh keys", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserListKeys("_any", "admin") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserAddKey() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with add key data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "add ssh key", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserAddKey("_any", "admin", osapi.SSHKeyAddOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserRemoveKey() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with remove key data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "remove ssh key", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserRemoveKey("_any", "admin", "SHA256:abc123") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestUserChangePassword() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with change password data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "change password", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.UserChangePassword("_any", "admin", "newpass") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestGroupList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with group list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list groups", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.GroupList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestGroupGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with group data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get group", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.GroupGet("_any", "admins") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestGroupCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with group create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create group", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.GroupCreate("_any", osapi.GroupCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestGroupUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with group update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update group", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.GroupUpdate("_any", "admins", osapi.GroupUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestGroupDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with group delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete group", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.GroupDelete("_any", "admins") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestCertificateList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with certificate list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list certificates", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.CertificateList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestCertificateCreate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with certificate create data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "create certificate", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.CertificateCreate("_any", osapi.CertificateCreateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestCertificateUpdate() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with certificate update data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "update certificate", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.CertificateUpdate("_any", "my-ca", osapi.CertificateUpdateOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestCertificateDelete() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with certificate delete data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "delete certificate", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.CertificateDelete("_any", "my-ca") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestProcessList() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with process list data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list processes", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ProcessList("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestProcessGet() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with process data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "get process", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ProcessGet("_any", 1234) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestProcessSignal() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with process signal data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "signal process", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.ProcessSignal("_any", 1234, osapi.ProcessSignalOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPowerReboot() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with reboot data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "reboot", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PowerReboot("_any", osapi.PowerOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestPowerShutdown() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with shutdown data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":true}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "shutdown", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.PowerShutdown("_any", osapi.PowerOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestLogQuery() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with log query data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "query log", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.LogQuery("_any", osapi.LogQueryOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestLogSources() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with log sources data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "list log sources", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.LogSources("_any") + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + +func (s *OpsTestSuite) TestLogQueryUnit() { + tests := []struct { + name string + handler http.HandlerFunc + expectErr bool + errContains string + }{ + { + name: "Returns success with log query unit data", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write( + []byte( + `{"results":[{"hostname":"web-01","changed":false}],"job_id":"550e8400-e29b-41d4-a716-446655440000"}`, + ), + ) + }), + }, + { + name: "Returns error on auth failure", + handler: http.HandlerFunc(func( + w http.ResponseWriter, + _ *http.Request, + ) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) + }), + expectErr: true, + errContains: "query log unit", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func() { + server := httptest.NewServer(tc.handler) + defer server.Close() + + client := osapi.New(server.URL, "test-token") + + orch := New(server.URL, "test-token") + step := orch.LogQueryUnit("_any", "nginx.service", osapi.LogQueryOpts{}) + fn := step.task.Fn() + s.Require().NotNil(fn) + + result, fnErr := fn(context.Background(), client) + + if tc.expectErr { + s.Require().Error(fnErr) + s.Contains(fnErr.Error(), tc.errContains) + s.Nil(result) + + return + } + + s.Require().NoError(fnErr) + s.NotNil(result.Data) + s.Len(result.HostResults, 1) + }) + } +} + func TestOpsTestSuite( t *testing.T, ) { From 13878864e29b6400e5086454bc7c49f93b567f58 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: Mon, 6 Apr 2026 13:29:39 -0700 Subject: [PATCH 4/4] chore: fix repo consistency across docs, examples, and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update README and CLAUDE.md to reflect all 101 operations across 21 domains. Add missing operations to examples so every constructor is demonstrated. Create group.go example. Fix docker.go cleanup to use separate orchestrator pattern. Fix import order in test files. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 39 ++++++---- README.md | 37 +++++++--- examples/operations/agent-drain.go | 4 +- examples/operations/certificate.go | 6 +- examples/operations/cron.go | 8 +- examples/operations/dns-update.go | 5 +- examples/operations/docker.go | 54 +++----------- examples/operations/file-deploy.go | 8 +- examples/operations/group.go | 82 +++++++++++++++++++++ examples/operations/interface.go | 11 ++- examples/operations/package.go | 10 ++- examples/operations/power.go | 9 ++- examples/operations/process.go | 7 +- examples/operations/route.go | 11 ++- examples/operations/service.go | 24 ++---- examples/operations/sysctl.go | 6 +- examples/operations/user.go | 33 +++++---- pkg/orchestrator/host_status_public_test.go | 3 +- pkg/orchestrator/options_public_test.go | 3 +- 19 files changed, 238 insertions(+), 122 deletions(-) create mode 100644 examples/operations/group.go diff --git a/CLAUDE.md b/CLAUDE.md index afba51f..6d30852 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,19 +191,32 @@ Add the operation to the table in the domain landing page Add the operation to an existing workflow example in `examples/operations/` that covers the same domain. Domain groupings: -| Domain | Example file | -| ------- | -------------------- | -| Node | `node-info.go` | -| Node | `hostname-update.go` | -| Network | `dns-update.go` | -| Network | `ping.go` | -| Command | `command.go` | -| File | `file-deploy.go` | -| File | `file-changed.go` | -| Agent | `agent-drain.go` | -| Docker | `docker.go` | -| Cron | `cron.go` | -| Health | (used as gate) | +| Domain | Example file | +| ----------- | -------------------- | +| Node | `node-info.go` | +| Node | `hostname-update.go` | +| Network | `dns-update.go` | +| Network | `ping.go` | +| Interface | `interface.go` | +| Route | `route.go` | +| Command | `command.go` | +| File | `file-deploy.go` | +| File | `file-changed.go` | +| Agent | `agent-drain.go` | +| Docker | `docker.go` | +| Cron | `cron.go` | +| Sysctl | `sysctl.go` | +| NTP | `ntp.go` | +| Timezone | `timezone.go` | +| Service | `service.go` | +| Package | `package.go` | +| User | `user.go` | +| Group | `group.go` | +| Certificate | `certificate.go` | +| Process | `process.go` | +| Power | `power.go` | +| Log | `log.go` | +| Health | (used as gate) | If no domain match exists, create a new `{domain}.go` file. Every operation must appear in at least one runnable example. diff --git a/README.md b/README.md index a087e4a..e46b27b 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,31 @@ the request: ## ⚙️ Operations -37 typed constructors across 8 domains: - -| Domain | Docs | Example | -| ------ | ---- | ------- | -| Node | [8 operations](docs/operations/node/) | [node-info.go](examples/operations/node-info.go) | -| Network | [3 operations](docs/operations/network/) | [dns-update.go](examples/operations/dns-update.go) | -| Command | [2 operations](docs/operations/command/) | [command.go](examples/operations/command.go) | -| Docker | [9 operations](docs/operations/docker/) | [docker.go](examples/operations/docker.go) | -| Cron | [5 operations](docs/operations/cron/) | [cron.go](examples/operations/cron.go) | -| File | [5 operations](docs/operations/file/) | [file-deploy.go](examples/operations/file-deploy.go) | -| Agent | [4 operations](docs/operations/agent/) | [agent-drain.go](examples/operations/agent-drain.go) | -| Health | [1 operation](docs/operations/health/) | [basic.go](examples/features/basic.go) | +101 typed constructors across 21 domains: + +| Domain | Docs | Example | +| ----------- | -------------------------------------------- | ---------------------------------------------------- | +| Node | [8 operations](docs/operations/node/) | [node-info.go](examples/operations/node-info.go) | +| Network | [4 operations](docs/operations/network/) | [dns-update.go](examples/operations/dns-update.go) | +| Interface | [5 operations](docs/operations/interface/) | [interface.go](examples/operations/interface.go) | +| Route | [5 operations](docs/operations/route/) | [route.go](examples/operations/route.go) | +| Command | [2 operations](docs/operations/command/) | [command.go](examples/operations/command.go) | +| Docker | [9 operations](docs/operations/docker/) | [docker.go](examples/operations/docker.go) | +| Cron | [5 operations](docs/operations/cron/) | [cron.go](examples/operations/cron.go) | +| Sysctl | [5 operations](docs/operations/sysctl/) | [sysctl.go](examples/operations/sysctl.go) | +| NTP | [4 operations](docs/operations/ntp/) | [ntp.go](examples/operations/ntp.go) | +| Timezone | [2 operations](docs/operations/timezone/) | [timezone.go](examples/operations/timezone.go) | +| Service | [10 operations](docs/operations/service/) | [service.go](examples/operations/service.go) | +| Package | [6 operations](docs/operations/package/) | [package.go](examples/operations/package.go) | +| User | [9 operations](docs/operations/user/) | [user.go](examples/operations/user.go) | +| Group | [5 operations](docs/operations/group/) | [group.go](examples/operations/group.go) | +| Certificate | [4 operations](docs/operations/certificate/) | [certificate.go](examples/operations/certificate.go) | +| Process | [3 operations](docs/operations/process/) | [process.go](examples/operations/process.go) | +| Power | [2 operations](docs/operations/power/) | [power.go](examples/operations/power.go) | +| Log | [3 operations](docs/operations/log/) | [log.go](examples/operations/log.go) | +| File | [5 operations](docs/operations/file/) | [file-deploy.go](examples/operations/file-deploy.go) | +| Agent | [4 operations](docs/operations/agent/) | [agent-drain.go](examples/operations/agent-drain.go) | +| Health | [1 operation](docs/operations/health/) | [basic.go](examples/features/basic.go) | ## ✨ Features diff --git a/examples/operations/agent-drain.go b/examples/operations/agent-drain.go index d398502..1de1b56 100644 --- a/examples/operations/agent-drain.go +++ b/examples/operations/agent-drain.go @@ -86,7 +86,9 @@ func main() { // Drain → maintenance → undrain. o2 := orchestrator.New(url, token) - drain := o2.AgentDrain(host) + inspect := o2.AgentGet(host) + + drain := o2.AgentDrain(host).After(inspect) maint := o2.CommandExec(host, "echo", "maintenance complete"). After(drain) diff --git a/examples/operations/certificate.go b/examples/operations/certificate.go index 6992ef3..e66942b 100644 --- a/examples/operations/certificate.go +++ b/examples/operations/certificate.go @@ -78,7 +78,11 @@ func main() { list := o.CertificateList("_any").After(create) - o.CertificateDelete("_any", certName).After(list) + update := o.CertificateUpdate("_any", certName, osapi.CertificateUpdateOpts{ + Object: "osapi-example-ca.pem", + }).After(list) + + o.CertificateDelete("_any", certName).After(update) report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/cron.go b/examples/operations/cron.go index fb15940..31af986 100644 --- a/examples/operations/cron.go +++ b/examples/operations/cron.go @@ -77,7 +77,13 @@ func main() { get := o.CronGet("_any", "daily-cleanup").After(create) - o.CronDelete("_any", "daily-cleanup").After(get) + list := o.CronList("_any").After(get) + + update := o.CronUpdate("_any", "daily-cleanup", osapi.CronUpdateOpts{ + Schedule: "0 3 * * 0", + }).After(list) + + o.CronDelete("_any", "daily-cleanup").After(update) report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/dns-update.go b/examples/operations/dns-update.go index 1c2e46f..abf7ddd 100644 --- a/examples/operations/dns-update.go +++ b/examples/operations/dns-update.go @@ -70,13 +70,16 @@ func main() { ContinueOnError() // Write new DNS servers after reading the current config. - o.NetworkDNSUpdate( + updateDNS := o.NetworkDNSUpdate( "_any", iface, []string{"8.8.8.8", "8.8.4.4"}, []string{"example.com"}, ).After(getDNS).ContinueOnError() + // Delete DNS configuration (cleanup). + o.NetworkDNSDelete("_any", iface).After(updateDNS).ContinueOnError() + report, err := o.Run(context.Background()) if err != nil { log.Fatal(err) diff --git a/examples/operations/docker.go b/examples/operations/docker.go index a24894c..a993b20 100644 --- a/examples/operations/docker.go +++ b/examples/operations/docker.go @@ -19,23 +19,7 @@ // DEALINGS IN THE SOFTWARE. // Package main demonstrates the Docker container lifecycle: -// pull → create → start → list + exec + inspect → stop → remove → image remove. -// -// Pre-cleanup removes any leftover container and image so the example -// is repeatable. Requires Docker on the target host. -// -// DAG: -// -// pre-cleanup -// └── docker-pull -// └── docker-create -// └── docker-start -// ├── docker-list -// ├── docker-exec -// └── docker-inspect -// └── docker-stop -// └── docker-remove -// └── docker-image-remove +// pull → create → start → exec + inspect → stop → remove → image remove. // // Run with: OSAPI_TOKEN="" go run docker.go package main @@ -67,30 +51,18 @@ func main() { target = "_any" ) - o := orchestrator.New(url, token) + // Cleanup: remove leftover container and image. + cleanup := orchestrator.New(url, token) + cleanup.DockerRemove(target, name, &osapi.DockerRemoveParams{Force: true}). + ContinueOnError() + cleanup.DockerImageRemove(target, image, &osapi.DockerImageRemoveParams{Force: true}). + ContinueOnError() + _, _ = cleanup.Run(context.Background()) - // Pre-cleanup: remove leftover container and image. - preCleanup := o.TaskFunc( - "pre-cleanup", - func( - ctx context.Context, - c *osapi.Client, - _ orchestrator.Results, - ) (*orchestrator.Result, error) { - _, _ = c.Docker.Remove(ctx, target, name, &osapi.DockerRemoveParams{Force: true}) - _, _ = c.Docker.ImageRemove( - ctx, - target, - image, - &osapi.DockerImageRemoveParams{Force: true}, - ) - - return &orchestrator.Result{Changed: false}, nil - }, - ) + // Main workflow. + o := orchestrator.New(url, token) - pull := o.DockerPull(target, osapi.DockerPullOpts{Image: image}). - After(preCleanup) + pull := o.DockerPull(target, osapi.DockerPullOpts{Image: image}) create := o.DockerCreate(target, osapi.DockerCreateOpts{ Image: image, @@ -99,14 +71,12 @@ func main() { start := o.DockerStart(target, name).After(create) - // List, exec, and inspect run in parallel after start. - o.DockerList(target, nil).After(start) - exec := o.DockerExec(target, name, osapi.DockerExecOpts{ Command: []string{"nginx", "-v"}, }).After(start) inspect := o.DockerInspect(target, name).After(start) + o.DockerList(target, nil).After(start) stop := o.DockerStop(target, name, osapi.DockerStopOpts{}). After(exec, inspect) diff --git a/examples/operations/file-deploy.go b/examples/operations/file-deploy.go index 9e940b1..c31b482 100644 --- a/examples/operations/file-deploy.go +++ b/examples/operations/file-deploy.go @@ -18,13 +18,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package main demonstrates file deployment with idempotency proof -// and proper undeploy lifecycle. -// -// Phase 1: cleanup — remove any previously deployed file. -// Phase 2: first deploy — upload + deploy + verify (expect changed). -// Phase 3: idempotency — same upload + deploy + verify (expect unchanged). -// Phase 4: undeploy — remove the deployed file using FileUndeploy. +// Package main demonstrates file deploy, idempotency check, and undeploy. // // Run with: OSAPI_TOKEN="" go run file-deploy.go package main diff --git a/examples/operations/group.go b/examples/operations/group.go new file mode 100644 index 0000000..5e80755 --- /dev/null +++ b/examples/operations/group.go @@ -0,0 +1,82 @@ +// 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 local group management: +// list → create → get → update → delete. +// +// Run with: OSAPI_TOKEN="" go run group.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + osapi "github.com/retr0h/osapi/pkg/sdk/client" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" +) + +func main() { + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + const groupName = "osapi-example-group" + + // Cleanup any leftover group from a previous run. + oc := orchestrator.New(url, token) + oc.GroupDelete("_any", groupName).ContinueOnError() + oc.Run(context.Background()) //nolint:errcheck + + // List → create → get → update → delete. + o := orchestrator.New(url, token) + + list := o.GroupList("_any") + + create := o.GroupCreate("_any", osapi.GroupCreateOpts{ + Name: groupName, + }).After(list) + + get := o.GroupGet("_any", groupName).After(create) + + update := o.GroupUpdate("_any", groupName, osapi.GroupUpdateOpts{}). + After(get).ContinueOnError() + + o.GroupDelete("_any", groupName).After(update) + + report, err := o.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + var info osapi.GroupInfoResult + if err := report.Decode("get-group", &info); err == nil && len(info.Groups) > 0 { + g := info.Groups[0] + fmt.Printf("Group %q: gid=%d\n", g.Name, g.GID) + } +} diff --git a/examples/operations/interface.go b/examples/operations/interface.go index 5ca5129..365bb56 100644 --- a/examples/operations/interface.go +++ b/examples/operations/interface.go @@ -63,7 +63,16 @@ func main() { list := o.InterfaceList("_any").ContinueOnError() - o.InterfaceGet("_any", iface).After(list).ContinueOnError() + get := o.InterfaceGet("_any", iface).After(list).ContinueOnError() + + // Create → update → delete a test interface. + create := o.InterfaceCreate("_any", "osapi-test0", osapi.InterfaceConfigOpts{}). + After(get).ContinueOnError() + + update := o.InterfaceUpdate("_any", "osapi-test0", osapi.InterfaceConfigOpts{}). + After(create).ContinueOnError() + + o.InterfaceDelete("_any", "osapi-test0").After(update).ContinueOnError() report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/package.go b/examples/operations/package.go index e2090e1..c04ef76 100644 --- a/examples/operations/package.go +++ b/examples/operations/package.go @@ -63,16 +63,20 @@ func main() { oc.PackageRemove("_any", pkg).ContinueOnError() oc.Run(context.Background()) //nolint:errcheck - // Install → get → list updates → remove. + // List → install → get → list updates → update → remove. o := orchestrator.New(url, token) - install := o.PackageInstall("_any", pkg).ContinueOnError() + list := o.PackageList("_any").ContinueOnError() + + install := o.PackageInstall("_any", pkg).After(list).ContinueOnError() get := o.PackageGet("_any", pkg).After(install).ContinueOnError() updates := o.PackageListUpdates("_any").After(get).ContinueOnError() - o.PackageRemove("_any", pkg).After(updates).ContinueOnError() + update := o.PackageUpdate("_any").After(updates).ContinueOnError() + + o.PackageRemove("_any", pkg).After(update).ContinueOnError() report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/power.go b/examples/operations/power.go index 115606d..e52ecc0 100644 --- a/examples/operations/power.go +++ b/examples/operations/power.go @@ -57,11 +57,18 @@ func main() { // Schedule a reboot with a 60-second delay. // WARNING: this will reboot the target host. - o.PowerReboot("_any", osapi.PowerOpts{ + reboot := o.PowerReboot("_any", osapi.PowerOpts{ Delay: 60, Message: "OSAPI orchestrator example reboot", }).ContinueOnError() + // Schedule a shutdown (also delayed). + // WARNING: this will shut down the target host. + o.PowerShutdown("_any", osapi.PowerOpts{ + Delay: 120, + Message: "OSAPI orchestrator example shutdown", + }).After(reboot).ContinueOnError() + report, err := o.Run(context.Background()) if err != nil { log.Fatal(err) diff --git a/examples/operations/process.go b/examples/operations/process.go index 35801ae..a3ebb61 100644 --- a/examples/operations/process.go +++ b/examples/operations/process.go @@ -55,7 +55,12 @@ func main() { list := o.ProcessList("_any") - o.ProcessGet("_any", 1).After(list) + get := o.ProcessGet("_any", 1).After(list) + + // Send SIGCONT to PID 1 (harmless no-op for init). + o.ProcessSignal("_any", 1, osapi.ProcessSignalOpts{ + Signal: "SIGCONT", + }).After(get).ContinueOnError() report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/route.go b/examples/operations/route.go index 41a1bf9..ed4cf49 100644 --- a/examples/operations/route.go +++ b/examples/operations/route.go @@ -63,7 +63,16 @@ func main() { list := o.RouteList("_any").ContinueOnError() - o.RouteGet("_any", iface).After(list).ContinueOnError() + get := o.RouteGet("_any", iface).After(list).ContinueOnError() + + // Create → update → delete a test route. + create := o.RouteCreate("_any", iface, osapi.RouteConfigOpts{}). + After(get).ContinueOnError() + + update := o.RouteUpdate("_any", iface, osapi.RouteConfigOpts{}). + After(create).ContinueOnError() + + o.RouteDelete("_any", iface).After(update).ContinueOnError() report, err := o.Run(context.Background()) if err != nil { diff --git a/examples/operations/service.go b/examples/operations/service.go index f56bb51..9699804 100644 --- a/examples/operations/service.go +++ b/examples/operations/service.go @@ -18,22 +18,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -// Package main demonstrates systemd service lifecycle management: -// upload a unit file, create the service, start, enable, then -// cleanup: disable, stop, delete. -// -// Cleanup at the start ensures repeatability. -// -// DAG: -// -// upload-file -// └── create-service -// └── start-service -// └── enable-service -// └── get-service -// └── disable-service -// └── stop-service -// └── delete-service +// Package main demonstrates systemd service lifecycle: +// create → start → enable → get → list → restart → disable → stop → delete. // // Run with: OSAPI_TOKEN="" go run service.go package main @@ -98,7 +84,11 @@ WantedBy=multi-user.target get := o.ServiceGet("_any", svcName).After(enable) - disable := o.ServiceDisable("_any", svcName).After(get) + list := o.ServiceList("_any").After(get) + + restart := o.ServiceRestart("_any", svcName).After(list) + + disable := o.ServiceDisable("_any", svcName).After(restart) stop := o.ServiceStop("_any", svcName).After(disable) diff --git a/examples/operations/sysctl.go b/examples/operations/sysctl.go index a9d704b..3eba2be 100644 --- a/examples/operations/sysctl.go +++ b/examples/operations/sysctl.go @@ -62,13 +62,15 @@ func main() { oc.SysctlDelete("_any", key).ContinueOnError() oc.Run(context.Background()) //nolint:errcheck - // Create → get → update → delete. + // List → create → get → update → delete. o := orchestrator.New(url, token) + syslist := o.SysctlList("_any") + create := o.SysctlCreate("_any", osapi.SysctlCreateOpts{ Key: key, Value: "0", - }) + }).After(syslist) get := o.SysctlGet("_any", key).After(create) diff --git a/examples/operations/user.go b/examples/operations/user.go index 8568834..43055e4 100644 --- a/examples/operations/user.go +++ b/examples/operations/user.go @@ -19,18 +19,7 @@ // DEALINGS IN THE SOFTWARE. // Package main demonstrates user account management: -// create a user, add an SSH key, list keys, remove the key, then -// delete the user. -// -// Cleanup at the start ensures repeatability. -// -// DAG: -// -// create-user -// └── add-ssh-key -// └── list-ssh-key -// └── remove-ssh-key -// └── delete-user +// list → create → get → update → keys → change password → delete. // // Run with: OSAPI_TOKEN="" go run user.go package main @@ -67,17 +56,26 @@ func main() { oc.UserDelete("_any", username).ContinueOnError() oc.Run(context.Background()) //nolint:errcheck - // Create user → add key → list keys → remove key → delete user. + // List → create → get → update → add key → list keys → remove key → + // change password → delete. o := orchestrator.New(url, token) + userList := o.UserList("_any") + create := o.UserCreate("_any", osapi.UserCreateOpts{ Name: username, Shell: "/bin/bash", - }) + }).After(userList) + + get := o.UserGet("_any", username).After(create) + + update := o.UserUpdate("_any", username, osapi.UserUpdateOpts{ + Shell: "/bin/sh", + }).After(get).ContinueOnError() addKey := o.UserAddKey("_any", username, osapi.SSHKeyAddOpts{ Key: sshKey, - }).After(create) + }).After(update) listKeys := o.UserListKeys("_any", username).After(addKey) @@ -86,7 +84,10 @@ func main() { removeKey := o.UserRemoveKey("_any", username, "SHA256:example"). After(listKeys).ContinueOnError() - o.UserDelete("_any", username).After(removeKey) + chpass := o.UserChangePassword("_any", username, "temp-pass-123"). + After(removeKey).ContinueOnError() + + o.UserDelete("_any", username).After(chpass) report, err := o.Run(context.Background()) if err != nil { diff --git a/pkg/orchestrator/host_status_public_test.go b/pkg/orchestrator/host_status_public_test.go index 569a427..de6ffb2 100644 --- a/pkg/orchestrator/host_status_public_test.go +++ b/pkg/orchestrator/host_status_public_test.go @@ -23,8 +23,9 @@ package orchestrator_test import ( "testing" - "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" ) type HostStatusPublicTestSuite struct { diff --git a/pkg/orchestrator/options_public_test.go b/pkg/orchestrator/options_public_test.go index 0963099..88cc63b 100644 --- a/pkg/orchestrator/options_public_test.go +++ b/pkg/orchestrator/options_public_test.go @@ -25,8 +25,9 @@ import ( "net/http/httptest" "testing" - "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" "github.com/stretchr/testify/suite" + + "github.com/osapi-io/osapi-orchestrator/pkg/orchestrator" ) type OptionsPublicTestSuite struct {