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/docs/operations/README.md b/docs/operations/README.md index 17e8338..3b8d3cd 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..6767549 --- /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..64b641b --- /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..19e1aad --- /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..719aafa --- /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..74cdc19 --- /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..8237a25 --- /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..d3d1ba1 --- /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..7d03b00 --- /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..ccd7d59 --- /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..fb34d46 --- /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..de862bb --- /dev/null +++ b/docs/operations/interface/README.md @@ -0,0 +1,26 @@ +# 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..b33bbf9 --- /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..08c9660 --- /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..9ba7fae --- /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..a14526e --- /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..d29c3c2 --- /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..528436b --- /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..a5765c8 --- /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..be6b417 --- /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..8e81c37 --- /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..0399918 --- /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..2d25e6c --- /dev/null +++ b/docs/operations/package/README.md @@ -0,0 +1,27 @@ +# 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..e573806 --- /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..5e19bff --- /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..587d8b8 --- /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..5048a37 --- /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..e4ecfcb --- /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..6454bcc --- /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..f2a27e8 --- /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..1a4148f --- /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..874adaa --- /dev/null +++ b/docs/operations/process/README.md @@ -0,0 +1,23 @@ +# 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..0927c18 --- /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..75cb952 --- /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..d985adf --- /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..453ab2d --- /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..df8376f --- /dev/null +++ b/docs/operations/service/README.md @@ -0,0 +1,31 @@ +# 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..554fd3c --- /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..e6c3f5d --- /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..3133ec4 --- /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..df65adb --- /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..948c86c --- /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..0b2e7dc --- /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..5b8fba4 --- /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..5f513eb --- /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..72c9932 --- /dev/null +++ b/docs/operations/sysctl/README.md @@ -0,0 +1,26 @@ +# 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..1110552 --- /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..661dcc3 --- /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..ec51ae2 --- /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..11a0ed7 --- /dev/null +++ b/docs/operations/timezone/README.md @@ -0,0 +1,23 @@ +# 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..9bdea76 --- /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..297fbda --- /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..8f8bcc5 --- /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..ec6c15d --- /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..c8631a2 --- /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..c0beb15 --- /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..861f8a4 --- /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..098990d --- /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. 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/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 new file mode 100644 index 0000000..e66942b --- /dev/null +++ b/examples/operations/certificate.go @@ -0,0 +1,99 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates 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) + + 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 { + 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/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/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/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 new file mode 100644 index 0000000..365bb56 --- /dev/null +++ b/examples/operations/interface.go @@ -0,0 +1,99 @@ +// Copyright (c) 2026 John Dewey + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Package main demonstrates 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() + + 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 { + 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..c04ef76 --- /dev/null +++ b/examples/operations/package.go @@ -0,0 +1,93 @@ +// 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 + + // List → install → get → list updates → update → remove. + o := orchestrator.New(url, token) + + 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() + + update := o.PackageUpdate("_any").After(updates).ContinueOnError() + + o.PackageRemove("_any", pkg).After(update).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..e52ecc0 --- /dev/null +++ b/examples/operations/power.go @@ -0,0 +1,84 @@ +// 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. + 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) + } + + 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..a3ebb61 --- /dev/null +++ b/examples/operations/process.go @@ -0,0 +1,80 @@ +// 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") + + 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 { + 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..ed4cf49 --- /dev/null +++ b/examples/operations/route.go @@ -0,0 +1,92 @@ +// 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() + + 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 { + 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..9699804 --- /dev/null +++ b/examples/operations/service.go @@ -0,0 +1,107 @@ +// 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: +// create → start → enable → get → list → restart → disable → stop → delete. +// +// 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) + + 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) + + 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..3eba2be --- /dev/null +++ b/examples/operations/sysctl.go @@ -0,0 +1,92 @@ +// 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 + + // 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) + + 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..43055e4 --- /dev/null +++ b/examples/operations/user.go @@ -0,0 +1,104 @@ +// 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: +// list → create → get → update → keys → change password → delete. +// +// 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 + + // 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(update) + + 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() + + 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 { + 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/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/ops.go b/pkg/orchestrator/ops.go index 204dcc2..a070b2f 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, } }, 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, ) { 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 {