Skip to main content

File Management

OSAPI can upload files to a central Object Store and deploy them to managed hosts with SHA-based idempotency. File operations run through the job system, so the API server never writes to the filesystem directly -- agents handle all deployment.

What It Does

OperationDescription
UploadStore a file (base64-encoded) in the NATS Object Store
ListList all files stored in the Object Store
GetRetrieve metadata for a specific stored file
DeleteRemove a file from the Object Store
DeployDeploy a file from Object Store to agent filesystem
UndeployRemove a deployed file from disk (state preserved)
StatusCheck whether a deployed file is in-sync or drifted

Upload / List / Get / Delete manage files in the central NATS Object Store. Files are stored by name and tracked with SHA-256 checksums. These operations are synchronous REST calls -- they do not go through the job system.

Deploy creates an asynchronous job that fetches the file from the Object Store and writes it to the target path on the agent's filesystem. Deploy supports optional file permissions (mode, owner, group) and Go template rendering.

Undeploy creates an asynchronous job that removes a previously deployed file from the agent's filesystem. The file-state KV record is preserved so the undeploy is auditable and a subsequent deploy can detect the change.

Status creates an asynchronous job that compares the current file on disk against its expected SHA-256 from the file-state KV bucket. It reports one of three states: in-sync, drifted, or missing.

How It Works

File Upload Flow

  1. The CLI (or SDK) computes a SHA-256 of the local file and queries the Object Store to check whether it already holds the same content. If the SHA matches, the upload is skipped entirely (no bytes sent over the network).
  2. If the file is new or the SHA differs, the CLI sends the file via a multipart upload.
  3. On the server side, if a file with the same name already exists and the content differs, the server rejects the upload with 409 Conflict unless ?force=true is passed.
  4. If the content is identical, the server returns changed: false without rewriting the object.
  5. With --force, both the SDK pre-check and the server-side digest guard are bypassed — the file is always written and changed: true is returned.

File Deploy Flow

Deploy follows the standard job processing flow. The agent fetches the file from Object Store, computes its SHA-256, and checks the file-state KV for a previous deploy. If the SHA matches, the file is skipped (idempotent no-op). If the content differs, the agent writes the file to disk and updates the file-state KV with the new SHA-256.

You can target a specific host, broadcast to all hosts with _all, or route by label.

File Undeploy Flow

osapi client node file undeploy --target HOST --path /etc/app/app.conf

Undeploy follows the standard job processing flow. The agent removes the file from the filesystem but preserves the file-state KV record so the operation is auditable. A subsequent deploy will write the file even if the content has not changed (since the file is now absent). If the file does not exist on disk, the operation returns changed: false.

SHA-Based Idempotency

Every deploy operation computes a SHA-256 of the file content and compares it against the previously deployed SHA stored in the file-state KV bucket. If the hashes match, the file is not rewritten. This makes repeated deploys safe and efficient -- only actual changes hit the filesystem.

The file-state KV has no TTL, so deploy state persists indefinitely until explicitly removed.

Protected Objects

Files stored under the osapi/ name prefix are protected. Both uploads and deletes to osapi/* names return 403 Forbidden. These objects are managed exclusively by osapi itself — the agent seeds them on startup from embedded templates and updates them automatically when a new osapi version ships with changes.

Protected objects are used by meta providers such as the cron provider, which references them at deploy time. The osapi/ prefix is reserved; use any other prefix for your own files.

Template Rendering

When content_type is set to template, the file content is processed as a Go text/template before being written to disk. The template context provides three top-level fields:

FieldDescription
.FactsAgent's collected system facts (map)
.VarsUser-supplied template variables (map)
.HostnameTarget agent's hostname (string)

Example Template

A configuration file that adapts to each host:

# Generated for {{ .Hostname }}
listen_address = {{ .Vars.listen_address }}
workers = {{ .Facts.cpu_count }}
arch = {{ .Facts.architecture }}

Deploy it with template variables:

osapi client node file deploy \
--object-name app.conf.tmpl \
--path /etc/app/app.conf \
--content-type template \
--var listen_address=0.0.0.0:8080 \
--target _all

Each agent renders the template with its own facts and hostname, so the same template produces host-specific configuration across a fleet.

Available Fact Keys

Facts are exposed as a flat map via JSON round-tripping of the agent's FactsRegistration. Use dot-syntax (.Facts.key) for keys that are valid Go identifiers:

KeyTypeDescriptionExample
architecturestringCPU architectureamd64, arm64
kernel_versionstringOS kernel version6.8.0-51
cpu_countnumberLogical CPU count8
fqdnstringFully qualified domain nameweb-01.lan
service_mgrstringInit systemsystemd
package_mgrstringSystem package managerapt
containerizedbooleanRunning inside a containertrue
primary_interfacestringDefault route interface nameeth0
interfaces[]objectNetwork interfaces(see below)
routes[]objectIP routing table(see below)

Access scalar facts with dot-syntax:

arch = {{ .Facts.architecture }}
cpus = {{ .Facts.cpu_count }}

For keys with underscores, both {{ .Facts.kernel_version }} and {{ index .Facts "kernel_version" }} work.

Missing key behavior

Templates use Go's missingkey=error option. Accessing a key that doesn't exist via dot-syntax (e.g., {{ .Vars.missing }} or {{ .Facts.bogus }}) causes the deploy to fail with an error rather than silently rendering <no value>.

However, {{ index .Facts "nonexistent" }} uses Go's built-in index function, which returns the zero value for the map's value type — rendering <no value> without an error. Prefer dot-syntax over index for fact access so that typos are caught at deploy time.

Meta Provider Templates

Domains that use file deployment (service management, certificate management) inherit template support automatically. When the uploaded object has content_type: template, the file provider renders it at deploy time — the meta provider does not need to specify the content type.

For example, a systemd unit file template:

[Unit]
Description=App on {{ .Hostname }}

[Service]
ExecStart=/usr/bin/app --cpus {{ .Facts.cpu_count }}

Upload as a template, then deploy via the service API:

osapi client file upload \
--name my-unit --file app.service --content-type template

osapi client node service create \
--target web-01 --name my-app --object my-unit

Staleness Detection

When an object is re-uploaded to the Object Store with new content, existing deployments become stale — the deployed file no longer matches the source. The file stale command detects this by comparing the SHA-256 hash of each deployment against the current object content.

osapi client file stale

This is a controller-side check that does not contact agents. It compares the file-state KV (which tracks what was deployed) against the Object Store (which has the current content). Use file deploy to bring stale deployments up to date.

Stale detection covers all providers that use the file provider:

  • Service unit files
  • CA certificates
  • Cron scripts
  • Direct file deployments

Configuration

File management uses two NATS infrastructure components in addition to the general job infrastructure:

  • Object Store (nats.objects) -- stores uploaded file content. Configured with bucket name, max size, storage backend, and chunk size.
  • File State KV (nats.file_state) -- tracks deploy state (SHA-256, path, timestamps) per host. Has no TTL -- state persists until explicitly removed.

See Configuration for the full reference.

nats:
objects:
bucket: 'file-objects'
max_bytes: 104857600
storage: 'file'
replicas: 1
max_chunk_size: 262144

file_state:
bucket: 'file-state'
storage: 'file'
replicas: 1

Permissions

File operations require file:* permissions. The admin and write roles include both file:read and file:write. The read role includes only file:read. See the API reference for the permission required by each endpoint.