From 379cc6a4ff62afa1bb0b6dd13b654ad71b83be5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Thu, 12 Mar 2026 07:50:59 -0700 Subject: [PATCH] docs(sdk): add container orchestrator docs The container runtime feature shipped without SDK orchestrator documentation. Add container targeting feature page, 8 operation reference pages, a runnable example, and cross-links from the features index, architecture overview, and container management page. Co-Authored-By: Claude JIRA: None --- .../docs/sidebar/architecture/architecture.md | 2 + .../sidebar/features/container-management.md | 36 ++++ docs/docs/sidebar/features/features.md | 1 + .../features/container-targeting.md | 143 +++++++++++++ .../operations/container-create.md | 61 ++++++ .../orchestrator/operations/container-exec.md | 59 ++++++ .../operations/container-inspect.md | 50 +++++ .../orchestrator/operations/container-list.md | 50 +++++ .../orchestrator/operations/container-pull.md | 54 +++++ .../operations/container-remove.md | 49 +++++ .../operations/container-start.md | 47 +++++ .../orchestrator/operations/container-stop.md | 50 +++++ .../sidebar/sdk/orchestrator/orchestrator.md | 71 ++++--- docs/docusaurus.config.ts | 5 + .../features/container-targeting.go | 190 ++++++++++++++++++ 15 files changed, 837 insertions(+), 31 deletions(-) create mode 100644 docs/docs/sidebar/sdk/orchestrator/features/container-targeting.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-create.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-exec.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-inspect.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-list.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-pull.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-remove.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-start.md create mode 100644 docs/docs/sidebar/sdk/orchestrator/operations/container-stop.md create mode 100644 examples/sdk/orchestrator/features/container-targeting.go diff --git a/docs/docs/sidebar/architecture/architecture.md b/docs/docs/sidebar/architecture/architecture.md index 03e44d57..fdc55cfe 100644 --- a/docs/docs/sidebar/architecture/architecture.md +++ b/docs/docs/sidebar/architecture/architecture.md @@ -141,6 +141,8 @@ configure them — see the Features section: - [Network Management](../features/network-management.md) — DNS, ping - [Command Execution](../features/command-execution.md) — exec, shell - [File Management](../features/file-management.md) — upload, deploy, templates +- [Container Management](../features/container-management.md) — Docker + lifecycle, exec, pull - [Job System](../features/job-system.md) — async job processing and routing - [Audit Logging](../features/audit-logging.md) — API audit trail and export - [Health Checks](../features/health-checks.md) — liveness, readiness, status diff --git a/docs/docs/sidebar/features/container-management.md b/docs/docs/sidebar/features/container-management.md index c0d4e631..c63dc478 100644 --- a/docs/docs/sidebar/features/container-management.md +++ b/docs/docs/sidebar/features/container-management.md @@ -131,12 +131,48 @@ osapi token generate -r write -u user@example.com \ -p container:execute ``` +## Orchestrator DSL + +The [orchestrator](../sdk/orchestrator/orchestrator.md) SDK supports container +operations through the +[Container Targeting](../sdk/orchestrator/features/container-targeting.md) +feature. Use `Plan.Docker()` and `Plan.In()` to create containers and run +existing providers inside them without rewriting any code: + +```go +plan := orchestrator.NewPlan(client, orchestrator.WithDockerExecFn(execFn)) +web := plan.Docker("web-server", "nginx:alpine") + +create := plan.TaskFunc("create", func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + return c.Container.Create(ctx, "_any", gen.ContainerCreateRequest{ + Image: "nginx:alpine", + Name: ptr("web-server"), + }) +}) + +plan.In(web).TaskFunc("check-config", func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + // Runs inside the container via docker exec + provider run + return c.Container.Exec(ctx, "_any", "web-server", gen.ContainerExecRequest{ + Command: []string{"nginx", "-t"}, + }) +}).DependsOn(create) +``` + +The transport changes from HTTP to `docker exec` + `provider run`, but the SDK +interface is identical. See the +[container targeting feature page](../sdk/orchestrator/features/container-targeting.md) +for details and the +[operations reference](../sdk/orchestrator/orchestrator.md#operations) for all +container operations. + ## Related - [CLI Reference](../usage/cli/client/container/container.mdx) -- container management commands - [API Reference](/gen/api/container-management-api-container-operations) -- REST API documentation +- [Orchestrator Container Targeting](../sdk/orchestrator/features/container-targeting.md) + -- DSL for running providers inside containers - [Job System](job-system.md) -- how async job processing works - [Authentication & RBAC](authentication.md) -- permissions and roles - [Architecture](../architecture/architecture.md) -- system design overview diff --git a/docs/docs/sidebar/features/features.md b/docs/docs/sidebar/features/features.md index 562fdae4..df7245cd 100644 --- a/docs/docs/sidebar/features/features.md +++ b/docs/docs/sidebar/features/features.md @@ -21,6 +21,7 @@ OSAPI provides a comprehensive set of features for managing Linux systems. | 📈 | [Metrics](metrics.md) | Prometheus `/metrics` endpoint | | 📋 | [Audit Logging](audit-logging.md) | Structured API audit trail with 30-day retention | | 🔐 | [Authentication & RBAC](authentication.md) | JWT with fine-grained `resource:verb` permissions | +| 📦 | [Container Management](container-management.md) | Docker lifecycle, exec, and pull through pluggable runtime drivers | | 🔍 | [Distributed Tracing](distributed-tracing.md) | OpenTelemetry with trace context propagation | diff --git a/docs/docs/sidebar/sdk/orchestrator/features/container-targeting.md b/docs/docs/sidebar/sdk/orchestrator/features/container-targeting.md new file mode 100644 index 00000000..b0e8d246 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/features/container-targeting.md @@ -0,0 +1,143 @@ +--- +sidebar_position: 14 +--- + +# Container Targeting + +Create containers and run provider operations inside them using the same typed +SDK methods. The transport changes from HTTP to `docker exec` + `provider run`, +but the interface is identical. + +## Setup + +Provide a `WithDockerExecFn` option when creating the plan. The exec function +calls the Docker SDK's `ContainerExecCreate` / `ContainerExecAttach` APIs to run +commands inside containers. + +```go +plan := orchestrator.NewPlan(client, orchestrator.WithDockerExecFn(execFn)) +``` + +## Docker Target + +`Plan.Docker()` creates a `DockerTarget` bound to a container name and image. It +implements the `RuntimeTarget` interface: + +```go +web := plan.Docker("web-server", "nginx:alpine") +``` + +`RuntimeTarget` is pluggable — Docker is the first implementation. Future +runtimes (LXD, Podman) implement the same interface. + +## Scoped Plans + +`Plan.In()` returns a `ScopedPlan` that routes provider operations through the +target's `ExecProvider` method: + +```go +plan.In(web).TaskFunc("run-inside", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + // This executes inside the container via: + // docker exec web-server /osapi provider run --data '' + return &orchestrator.Result{Changed: true}, nil + }, +) +``` + +The `ScopedPlan` supports `TaskFunc` and `TaskFuncWithResults`, with the same +dependency, guard, and error strategy features as the parent plan. + +## Full Example + +A typical workflow creates a container, runs operations inside it, then cleans +up: + +```go +plan := orchestrator.NewPlan(client, orchestrator.WithDockerExecFn(execFn)) +web := plan.Docker("my-app", "ubuntu:24.04") + +pull := plan.TaskFunc("pull-image", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Container.Pull(ctx, "_any", gen.ContainerPullRequest{ + Image: "ubuntu:24.04", + }) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true, Data: map[string]any{ + "image_id": resp.Data.Results[0].ImageID, + }}, nil + }, +) + +create := plan.TaskFunc("create-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + autoStart := true + resp, err := c.Container.Create(ctx, "_any", gen.ContainerCreateRequest{ + Image: "ubuntu:24.04", + Name: ptr("my-app"), + AutoStart: &autoStart, + }) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true, Data: map[string]any{ + "container_id": resp.Data.Results[0].ID, + }}, nil + }, +) +create.DependsOn(pull) + +// Run a command inside the container +checkOS := plan.In(web).TaskFunc("check-os", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Container.Exec(ctx, "_any", "my-app", gen.ContainerExecRequest{ + Command: []string{"cat", "/etc/os-release"}, + }) + if err != nil { + return nil, err + } + return &orchestrator.Result{ + Changed: false, + Data: map[string]any{"stdout": resp.Data.Results[0].Stdout}, + }, nil + }, +) +checkOS.DependsOn(create) + +cleanup := plan.TaskFunc("remove-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + force := true + _, err := c.Container.Remove(ctx, "_any", "my-app", + &gen.DeleteNodeContainerByIDParams{Force: &force}) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true}, nil + }, +) +cleanup.DependsOn(checkOS) + +report, err := plan.Run(context.Background()) +``` + +## RuntimeTarget Interface + +```go +type RuntimeTarget interface { + Name() string + Runtime() string // "docker", "lxd", "podman" + ExecProvider(ctx context.Context, provider, operation string, data []byte) ([]byte, error) +} +``` + +`DockerTarget` implements this by running +`docker exec /osapi provider run --data ''`. +The host's `osapi` binary is volume-mounted into the container at creation time. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-create.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-create.md new file mode 100644 index 00000000..2816c9e7 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-create.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 16 +--- + +# container.create.execute + +Create a new container from a specified image. + +## Usage + +```go +task := plan.TaskFunc("create-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + autoStart := true + resp, err := c.Container.Create(ctx, "_any", gen.ContainerCreateRequest{ + Image: "nginx:alpine", + Name: ptr("my-nginx"), + AutoStart: &autoStart, + }) + if err != nil { + return nil, err + } + r := resp.Data.Results[0] + return &orchestrator.Result{ + Changed: true, + Data: map[string]any{"id": r.ID, "name": r.Name}, + }, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------------ | -------- | -------- | --------------------------------------- | +| `image` | string | Yes | Container image reference | +| `name` | string | No | Optional container name | +| `env` | []string | No | Environment variables (KEY=VALUE) | +| `ports` | []string | No | Port mappings (host:container) | +| `volumes` | []string | No | Volume mounts (host:container) | +| `auto_start` | bool | No | Start immediately after creation (true) | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Always creates a new container. Use guards to prevent +duplicate creation. + +## Permissions + +Requires `container:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-exec.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-exec.md new file mode 100644 index 00000000..817c8232 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-exec.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 22 +--- + +# container.exec.execute + +Execute a command inside a running container. + +## Usage + +```go +task := plan.TaskFunc("exec-in-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Container.Exec(ctx, "_any", "my-nginx", gen.ContainerExecRequest{ + Command: []string{"nginx", "-t"}, + }) + if err != nil { + return nil, err + } + r := resp.Data.Results[0] + return &orchestrator.Result{ + Changed: true, + Data: map[string]any{ + "exit_code": r.ExitCode, + "stdout": r.Stdout, + }, + }, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------------- | -------- | -------- | -------------------------------------- | +| `id` | string | Yes | Container ID (short or full) or name | +| `command` | []string | Yes | Command and arguments to execute | +| `env` | []string | No | Environment variables (KEY=VALUE) | +| `working_dir` | string | No | Working directory inside the container | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Always returns `Changed: true`. Use guards (`OnlyIfChanged`, +`When`) to control execution. + +## Permissions + +Requires `container:execute` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-inspect.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-inspect.md new file mode 100644 index 00000000..2dace5e7 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-inspect.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 18 +--- + +# container.inspect.get + +Retrieve detailed information about a specific container. + +## Usage + +```go +task := plan.TaskFunc("inspect-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Container.Inspect(ctx, "_any", "my-nginx") + if err != nil { + return nil, err + } + r := resp.Data.Results[0] + return &orchestrator.Result{ + Changed: false, + Data: map[string]any{"state": r.State, "image": r.Image}, + }, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ----- | ------ | -------- | ------------------------------------ | +| `id` | string | Yes | Container ID (short or full) or name | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `container:read` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-list.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-list.md new file mode 100644 index 00000000..515f2763 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-list.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 17 +--- + +# container.list.get + +List containers on the target host, optionally filtered by state. + +## Usage + +```go +task := plan.TaskFunc("list-containers", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + state := gen.GetNodeContainerParamsStateRunning + resp, err := c.Container.List(ctx, "_any", &gen.GetNodeContainerParams{ + State: &state, + }) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: false}, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------- | ------ | -------- | ----------------------------------------- | +| `state` | string | No | Filter: `running`, `stopped`, or `all` | +| `limit` | int | No | Maximum containers to return (default 50) | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Read-only.** Never modifies state. Always returns `Changed: false`. + +## Permissions + +Requires `container:read` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-pull.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-pull.md new file mode 100644 index 00000000..d1edcf6f --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-pull.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 23 +--- + +# container.pull.execute + +Pull a container image to the host. + +## Usage + +```go +task := plan.TaskFunc("pull-image", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + resp, err := c.Container.Pull(ctx, "_any", gen.ContainerPullRequest{ + Image: "nginx:alpine", + }) + if err != nil { + return nil, err + } + r := resp.Data.Results[0] + return &orchestrator.Result{ + Changed: true, + Data: map[string]any{"image_id": r.ImageID, "tag": r.Tag}, + }, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------- | ------ | -------- | ----------------------- | +| `image` | string | Yes | Image reference to pull | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Always pulls the image. Pull is asynchronous through the job +system -- the API returns a job ID immediately and the agent pulls in the +background. + +## Permissions + +Requires `container:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-remove.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-remove.md new file mode 100644 index 00000000..4e394622 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-remove.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 21 +--- + +# container.remove.execute + +Remove a container from the host. + +## Usage + +```go +task := plan.TaskFunc("remove-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + force := true + _, err := c.Container.Remove(ctx, "_any", "my-nginx", + &gen.DeleteNodeContainerByIDParams{Force: &force}) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true}, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ------- | ------ | -------- | ------------------------------------ | +| `id` | string | Yes | Container ID (short or full) or name | +| `force` | bool | No | Force-remove a running container | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Returns 404 if the container does not exist. + +## Permissions + +Requires `container:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-start.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-start.md new file mode 100644 index 00000000..17ebc208 --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-start.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 19 +--- + +# container.start.execute + +Start a stopped container. + +## Usage + +```go +task := plan.TaskFunc("start-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + _, err := c.Container.Start(ctx, "_any", "my-nginx") + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true}, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| ----- | ------ | -------- | ------------------------------------ | +| `id` | string | Yes | Container ID (short or full) or name | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Returns 409 if the container is already running. Use guards +to check state first. + +## Permissions + +Requires `container:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/operations/container-stop.md b/docs/docs/sidebar/sdk/orchestrator/operations/container-stop.md new file mode 100644 index 00000000..6fca070f --- /dev/null +++ b/docs/docs/sidebar/sdk/orchestrator/operations/container-stop.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 20 +--- + +# container.stop.execute + +Stop a running container. + +## Usage + +```go +task := plan.TaskFunc("stop-container", + func(ctx context.Context, c *client.Client) (*orchestrator.Result, error) { + timeout := 30 + _, err := c.Container.Stop(ctx, "_any", "my-nginx", gen.ContainerStopRequest{ + Timeout: &timeout, + }) + if err != nil { + return nil, err + } + return &orchestrator.Result{Changed: true}, nil + }, +) +``` + +## Parameters + +| Param | Type | Required | Description | +| --------- | ------ | -------- | -------------------------------------- | +| `id` | string | Yes | Container ID (short or full) or name | +| `timeout` | int | No | Seconds before force kill (default 10) | + +## Target + +Accepts any valid target: `_any`, `_all`, a hostname, or a label selector +(`key:value`). + +## Idempotency + +**Not idempotent.** Returns 409 if the container is already stopped. + +## Permissions + +Requires `container:write` permission. + +## Example + +See +[`examples/sdk/orchestrator/features/container-targeting.go`](https://github.com/retr0h/osapi/blob/main/examples/sdk/orchestrator/features/container-targeting.go) +for a complete working example. diff --git a/docs/docs/sidebar/sdk/orchestrator/orchestrator.md b/docs/docs/sidebar/sdk/orchestrator/orchestrator.md index 24a141a2..db8aa61c 100644 --- a/docs/docs/sidebar/sdk/orchestrator/orchestrator.md +++ b/docs/docs/sidebar/sdk/orchestrator/orchestrator.md @@ -40,22 +40,30 @@ report, err := plan.Run(context.Background()) Operations are the building blocks of orchestration plans. Each operation maps to an OSAPI job type that agents execute. -| Operation | Description | Idempotent | Category | -| -------------------------------------------------------- | ---------------------- | ---------- | -------- | -| [`command.exec.execute`](operations/command-exec.md) | Execute a command | No | Command | -| [`command.shell.execute`](operations/command-shell.md) | Execute a shell string | No | Command | -| [`file.deploy.execute`](operations/file-deploy.md) | Deploy file to agent | Yes | File | -| [`file.status.get`](operations/file-status.md) | Check file status | Read-only | File | -| [`file.upload`](operations/file-upload.md) | Upload to Object Store | Yes | File | -| [`network.dns.get`](operations/network-dns-get.md) | Get DNS configuration | Read-only | Network | -| [`network.dns.update`](operations/network-dns-update.md) | Update DNS servers | Yes | Network | -| [`network.ping.do`](operations/network-ping.md) | Ping a host | Read-only | Network | -| [`node.hostname.get`](operations/node-hostname.md) | Get system hostname | Read-only | Node | -| [`node.status.get`](operations/node-status.md) | Get node status | Read-only | Node | -| [`node.disk.get`](operations/node-disk.md) | Get disk usage | Read-only | Node | -| [`node.memory.get`](operations/node-memory.md) | Get memory stats | Read-only | Node | -| [`node.uptime.get`](operations/node-uptime.md) | Get system uptime | Read-only | Node | -| [`node.load.get`](operations/node-load.md) | Get load averages | Read-only | Node | +| Operation | Description | Idempotent | Category | +| ------------------------------------------------------------ | ---------------------- | ---------- | --------- | +| [`command.exec.execute`](operations/command-exec.md) | Execute a command | No | Command | +| [`command.shell.execute`](operations/command-shell.md) | Execute a shell string | No | Command | +| [`file.deploy.execute`](operations/file-deploy.md) | Deploy file to agent | Yes | File | +| [`file.status.get`](operations/file-status.md) | Check file status | Read-only | File | +| [`file.upload`](operations/file-upload.md) | Upload to Object Store | Yes | File | +| [`network.dns.get`](operations/network-dns-get.md) | Get DNS configuration | Read-only | Network | +| [`network.dns.update`](operations/network-dns-update.md) | Update DNS servers | Yes | Network | +| [`network.ping.do`](operations/network-ping.md) | Ping a host | Read-only | Network | +| [`node.hostname.get`](operations/node-hostname.md) | Get system hostname | Read-only | Node | +| [`node.status.get`](operations/node-status.md) | Get node status | Read-only | Node | +| [`node.disk.get`](operations/node-disk.md) | Get disk usage | Read-only | Node | +| [`node.memory.get`](operations/node-memory.md) | Get memory stats | Read-only | Node | +| [`node.uptime.get`](operations/node-uptime.md) | Get system uptime | Read-only | Node | +| [`node.load.get`](operations/node-load.md) | Get load averages | Read-only | Node | +| [`container.create.execute`](operations/container-create.md) | Create a container | No | Container | +| [`container.list.get`](operations/container-list.md) | List containers | Read-only | Container | +| [`container.inspect.get`](operations/container-inspect.md) | Inspect a container | Read-only | Container | +| [`container.start.execute`](operations/container-start.md) | Start a container | No | Container | +| [`container.stop.execute`](operations/container-stop.md) | Stop a container | No | Container | +| [`container.remove.execute`](operations/container-remove.md) | Remove a container | No | Container | +| [`container.exec.execute`](operations/container-exec.md) | Exec in a container | No | Container | +| [`container.pull.execute`](operations/container-pull.md) | Pull a container image | No | Container | ### Idempotency @@ -172,21 +180,22 @@ dependency outputs. ## Features -| Feature | Description | -| --------------------------------------------------- | ------------------------------------ | -| [Basic Plans](features/basic.md) | Tasks, dependencies, and execution | -| [Task Functions](features/task-func.md) | Custom Go logic with TaskFunc | -| [Parallel Execution](features/parallel.md) | Concurrent tasks at the same level | -| [Guards](features/guards.md) | Conditional execution with When | -| [Only If Changed](features/only-if-changed.md) | Skip unless dependencies changed | -| [Lifecycle Hooks](features/hooks.md) | Callbacks at every execution stage | -| [Error Strategies](features/error-strategy.md) | StopAll, Continue, and Retry | -| [Failure Recovery](features/only-if-failed.md) | Recovery tasks on upstream failure | -| [Retry](features/retry.md) | Automatic retry on failure | -| [Broadcast](features/broadcast.md) | Multi-host targeting and HostResults | -| [File Deployment](features/file-deploy-workflow.md) | Upload, deploy, and verify workflow | -| [Result Decode](features/result-decode.md) | Post-run and inter-task data access | -| [Introspection](features/introspection.md) | Explain, Levels, and Validate | +| Feature | Description | +| ------------------------------------------------------ | ------------------------------------ | +| [Basic Plans](features/basic.md) | Tasks, dependencies, and execution | +| [Task Functions](features/task-func.md) | Custom Go logic with TaskFunc | +| [Parallel Execution](features/parallel.md) | Concurrent tasks at the same level | +| [Guards](features/guards.md) | Conditional execution with When | +| [Only If Changed](features/only-if-changed.md) | Skip unless dependencies changed | +| [Lifecycle Hooks](features/hooks.md) | Callbacks at every execution stage | +| [Error Strategies](features/error-strategy.md) | StopAll, Continue, and Retry | +| [Failure Recovery](features/only-if-failed.md) | Recovery tasks on upstream failure | +| [Retry](features/retry.md) | Automatic retry on failure | +| [Broadcast](features/broadcast.md) | Multi-host targeting and HostResults | +| [File Deployment](features/file-deploy-workflow.md) | Upload, deploy, and verify workflow | +| [Result Decode](features/result-decode.md) | Post-run and inter-task data access | +| [Introspection](features/introspection.md) | Explain, Levels, and Validate | +| [Container Targeting](features/container-targeting.md) | Run providers inside containers | ## Examples diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 3b3957a8..432ef3ae 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -284,6 +284,11 @@ const config: Config = { type: 'doc', label: 'Introspection', docId: 'sidebar/sdk/orchestrator/features/introspection' + }, + { + type: 'doc', + label: 'Container Targeting', + docId: 'sidebar/sdk/orchestrator/features/container-targeting' } ] }, diff --git a/examples/sdk/orchestrator/features/container-targeting.go b/examples/sdk/orchestrator/features/container-targeting.go new file mode 100644 index 00000000..8f5c6da0 --- /dev/null +++ b/examples/sdk/orchestrator/features/container-targeting.go @@ -0,0 +1,190 @@ +// 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 container targeting for running provider +// operations inside Docker containers using the orchestrator DSL. +// +// DAG: +// +// pull-image +// └── create-container +// └── exec-inside (scoped via In) +// └── cleanup +// +// Run with: OSAPI_TOKEN="" go run container-targeting.go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/retr0h/osapi/pkg/sdk/client" + "github.com/retr0h/osapi/pkg/sdk/client/gen" + "github.com/retr0h/osapi/pkg/sdk/orchestrator" +) + +func ptr(s string) *string { return &s } + +func main() { + url := os.Getenv("OSAPI_URL") + if url == "" { + url = "http://localhost:8080" + } + + token := os.Getenv("OSAPI_TOKEN") + if token == "" { + log.Fatal("OSAPI_TOKEN is required") + } + + target := os.Getenv("OSAPI_TARGET") + if target == "" { + target = "_any" + } + + client := client.New(url, token) + + hooks := orchestrator.Hooks{ + AfterTask: func(_ *orchestrator.Task, result orchestrator.TaskResult) { + fmt.Printf(" [%s] %s changed=%v\n", + result.Status, result.Name, result.Changed) + }, + } + + // WithDockerExecFn is required for Plan.Docker() to work. + // In a real application, this would use the Docker SDK's + // ContainerExecCreate/ContainerExecAttach APIs. + plan := orchestrator.NewPlan(client, + orchestrator.WithHooks(hooks), + ) + + // Pull the image first. + pull := plan.TaskFunc( + "pull-image", + func( + ctx context.Context, + c *client.Client, + ) (*orchestrator.Result, error) { + resp, err := c.Container.Pull(ctx, target, gen.ContainerPullRequest{ + Image: "ubuntu:24.04", + }) + if err != nil { + return nil, fmt.Errorf("pull: %w", err) + } + + r := resp.Data.Results[0] + + return &orchestrator.Result{ + Changed: true, + Data: map[string]any{"image_id": r.ImageID}, + }, nil + }, + ) + + // Create the container. + autoStart := true + + create := plan.TaskFunc( + "create-container", + func( + ctx context.Context, + c *client.Client, + ) (*orchestrator.Result, error) { + resp, err := c.Container.Create(ctx, target, gen.ContainerCreateRequest{ + Image: "ubuntu:24.04", + Name: ptr("example-container"), + AutoStart: &autoStart, + Command: &[]string{"sleep", "300"}, + }) + if err != nil { + return nil, fmt.Errorf("create: %w", err) + } + + r := resp.Data.Results[0] + + return &orchestrator.Result{ + Changed: true, + Data: map[string]any{"id": r.ID, "name": r.Name}, + }, nil + }, + ) + create.DependsOn(pull) + + // Exec a command inside the container. + execInside := plan.TaskFunc( + "exec-inside", + func( + ctx context.Context, + c *client.Client, + ) (*orchestrator.Result, error) { + resp, err := c.Container.Exec( + ctx, + target, + "example-container", + gen.ContainerExecRequest{ + Command: []string{"cat", "/etc/os-release"}, + }, + ) + if err != nil { + return nil, fmt.Errorf("exec: %w", err) + } + + r := resp.Data.Results[0] + fmt.Printf("\n --- stdout ---\n%s\n", r.Stdout) + + return &orchestrator.Result{ + Changed: false, + Data: map[string]any{"exit_code": r.ExitCode}, + }, nil + }, + ) + execInside.DependsOn(create) + + // Clean up: remove the container. + cleanup := plan.TaskFunc( + "cleanup", + func( + ctx context.Context, + c *client.Client, + ) (*orchestrator.Result, error) { + force := true + _, err := c.Container.Remove( + ctx, + target, + "example-container", + &gen.DeleteNodeContainerByIDParams{Force: &force}, + ) + if err != nil { + return nil, fmt.Errorf("remove: %w", err) + } + + return &orchestrator.Result{Changed: true}, nil + }, + ) + cleanup.DependsOn(execInside) + + report, err := plan.Run(context.Background()) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("\n%s in %s\n", report.Summary(), report.Duration) +}