Skip to main content

System Architecture

OSAPI is a Linux system management platform that exposes a REST API for querying and modifying host configuration and uses NATS JetStream for distributed, asynchronous job processing. Operators interact with the system through a CLI that can either hit the REST API directly or manage the job queue.

Component Map

The system is organized into six layers, top to bottom:

LayerPackageRole
CLIcmd/Cobra command tree (thin wiring)
Generated HTTP Clientinternal/client/OpenAPI-generated client used by CLI
REST APIinternal/api/Echo server with JWT middleware
Job Clientinternal/job/client/Business logic for job CRUD and status
NATS JetStream(external)KV job-queue, Stream JOBS, KV job-responses
Job Worker / Provider Layerinternal/job/worker/, internal/provider/Consumes jobs from NATS and executes providers

The CLI talks to the REST API through the generated HTTP client. The REST API delegates state-changing operations to the job client, which stores jobs in NATS KV and publishes notifications to the JOBS stream. Workers pick up notifications, execute the matching provider, and write results back to KV.

Entry Points

The osapi binary exposes four top-level command groups:

  • osapi api server start — starts the REST API server (Echo + JWT middleware)
  • osapi job worker start — starts a job worker that subscribes to NATS subjects and processes operations
  • osapi nats server start — starts an embedded NATS server with JetStream enabled
  • osapi client — CLI client that talks to the REST API (system, network, and job subcommands)

Layers

CLI (cmd/)

The CLI is a Cobra command tree. Each file maps to a single command (e.g., client_job_add.go implements osapi client job add). The CLI layer is thin wiring: it parses flags, reads config via Viper, and delegates to the appropriate internal package.

REST API (internal/api/)

The API server is built on Echo with handlers generated from an OpenAPI spec via oapi-codegen (*.gen.go files). Domain handlers are organized into subpackages:

PackageResponsibility
internal/api/system/System endpoints (hostname, status, uptime, disk, memory, load)
internal/api/network/Network endpoints (DNS, ping)
internal/api/job/Job queue endpoints (create, get, list, delete, status, workers)
internal/api/common/Shared middleware, error handling, collection responses

All state-changing operations are dispatched as jobs through the job client layer rather than executed inline. Responses follow a uniform collection envelope documented in the API Design Guidelines.

Job System (internal/job/)

The job system implements a KV-first, stream-notification architecture on NATS JetStream. Core types live in internal/job/, with two subpackages:

PackagePurpose
internal/job/client/High-level operations (create, status, query)
internal/job/worker/Consumer pipeline (subscribe, handle, process)

Subject routing uses dot-notation hierarchies (jobs.query.*, jobs.modify.*) with support for load-balanced (_any), broadcast (_all), direct-host, and label-based targeting.

For the full deep dive see Job System Architecture.

Provider Layer (internal/provider/)

Providers implement the actual system operations behind a common interface. Each provider is selected at runtime through a platform-aware factory pattern.

DomainProviders
system/hostHostname, uptime, OS info
system/diskDisk usage statistics
system/memMemory usage statistics
system/loadLoad average statistics
network/dnsDNS configuration (get/update)
network/pingPing execution and statistics

Providers are stateless and platform-specific (e.g., a Ubuntu DNS provider vs. a generic Linux DNS provider). Adding a new operation means implementing the provider interface and registering it in the worker's processor dispatch.

Configuration (internal/config/)

Configuration is managed by Viper and loaded from an osapi.yaml file. Environment variables override file values using the OSAPI_ prefix with underscore-separated keys (e.g., OSAPI_API_SERVER_PORT).

Minimal configuration skeleton (see Configuration for the full reference):

api:
client:
url: 'http://localhost:8080'
security:
bearer_token: '<jwt>'
server:
port: 8080
security:
signing_key: '<secret>'
cors:
allow_origins: []

nats:
server:
host: '0.0.0.0'
port: 4222
store_dir: '/tmp/nats-store'

job:
stream_name: 'JOBS'
kv_bucket: 'job-queue'
kv_response_bucket: 'job-responses'
consumer_name: 'jobs-worker'
worker:
hostname: 'worker-01'
labels:
group: 'web.dev'

Request Flow

A typical operation (e.g., system.hostname.get) follows these steps:

  1. CLI sends POST /api/v1/jobs to the REST API.
  2. REST API calls CreateJob() on the job client.
  3. Job Client stores the immutable job definition in NATS KV (job-queue) and publishes a notification to the JOBS stream.
  4. The API returns 201 with the job_id to the CLI.
  5. A Worker receives the stream notification, fetches the immutable job from KV, and executes the matching provider.
  6. The Worker writes status events and the result back to KV (job-responses).
  7. CLI polls GET /api/v1/jobs/{id}. The API reads computed status from KV events and returns the result.

Security

Authentication

The API uses JWT HS256 tokens signed with a shared secret (security.signing_key). Tokens carry a role claim that determines the caller's access level. The osapi token generate command creates tokens for a given role.

Authorization

Access control is scope-based with a three-tier role hierarchy:

RoleScopes
adminread, write, admin
writeread, write
readread

Each API endpoint declares a required scope. The JWT middleware checks that the caller's role includes that scope before allowing the request.

CORS

Cross-Origin Resource Sharing is configured per-server via api.server.security.cors.allow_origins in osapi.yaml. An empty list disables CORS headers entirely.

External Dependencies

DependencyPurpose
EchoHTTP framework for the REST API
Cobra / ViperCLI framework and configuration
NATS / JetStreamMessaging, KV store, stream processing
oapi-codegenOpenAPI strict-server code generation
gopsutilCross-platform system metrics
pro-bingICMP ping implementation
golang-jwtJWT creation and validation
nats-client / nats-serverSibling repos (linked via go.mod replace)

Further Reading