shiftlabs Start free
Menu Targets

Targets

Keep the devices, hosts, clusters and services your workflows act on as records of the workspace, not as lists in payloads. Each has an id that never changes, a version, and a history of every change.

A target

Every type takes the same fields, and only id, type and name are required, custom included.

jsonGET /v1/targets/core-sw-01
{
  "id": "core-sw-01",
  "type": "device",
  "name": "Core switch 1",
  "attributes": { "site": "dc-east", "vendor": "arista" },
  "groups": ["dc-east", "switches"],
  "haGroup": null,
  "version": 3,
  "createdAt": 1793000000000,
  "updatedAt": 1793000500000
}
  • id

    Unique in the workspace, 1 to 255 characters, and never changes. It isn't . or .., which a URL path can't name. A target's id can be passed as it is as the device of step.snapshot.

  • type

    device, host, cluster, service or custom.

  • name

    1 to 255 characters.

  • attributes

    Key/value strings, 64 at most: a key 1 to 64 characters, a value up to 1,024. Default {}.

  • groups

    Group names, 32 at most, each 1 to 64 characters. Each is kept once, and they come back sorted. Default [].

  • haGroup

    The HA group the target is in, up to 64 characters, or null, the default. It is kept as given; nothing acts on it yet.

  • version

    1 when the target is created, and one more with each change. createdAt and updatedAt are milliseconds since the epoch.

No field takes control characters, such as a line break or a tab. An id, name, attribute key, group or HA group can't be only spaces.

Create and update

Admins create and update targets, with the API, the CLI or the client. Every member, viewers included, can list and read them.

shellcurl
curl -X POST https://api.shiftlabs.org/v1/targets \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  -H "content-type: application/json" \
  -d '{"id": "core-sw-01", "type": "device", "name": "Core switch 1",
       "attributes": {"site": "dc-east", "vendor": "arista"}, "groups": ["dc-east", "switches"]}'

# Only while it's at version 1: every attribute to keep, and the new one.
curl -X PATCH https://api.shiftlabs.org/v1/targets/core-sw-01 \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" \
  -H "content-type: application/json" \
  -H 'if-match: "1"' \
  -d '{"attributes": {"site": "dc-east", "vendor": "arista", "rack": "r12"}}'
  • Create

    POST /v1/targets answers 201 with the target. An id the workspace already has answers 409 {"error": "target already exists", "target"} with that target, and changes nothing.

  • Update

    PATCH /v1/targets/:id takes the fields to change. Each replaces the target's whole field: attributes is every attribute the target keeps, and groups every group. "haGroup": null clears the HA group. An update that changes nothing answers the target as it is, at the same version.

  • The id stays

    A body's id, when it has one, must be the target's own, or it answers 400. To rename a target, change its name.

  • Versions

    Reads answer the version as ETag. Send it back as If-Match on PATCH: when the target changed since, it answers 412 {"error", "target"} with the target as it is now, and changes nothing. Without If-Match, an update applies to whatever version the target is at.

  • Invalid fields

    A field that breaks a rule above, or a field a target doesn't have, answers 400 {"error", "field"}, field naming it, and changes nothing.

  • Ids in paths

    Percent-encode the id into one path segment: core sw/1 is /v1/targets/core%20sw%2F1.

List and read

  • One target

    GET /v1/targets/:id answers the target, or 404 {"error": "target not found"}.

  • Pages

    GET /v1/targets lists the newest created first, 50 a page, ?limit= up to 100. While more remain, the Next-Before header names where the next page starts: pass it as ?before=. See Pages.

  • Filters

    ?type= keeps one type, and ?group= the targets in one group. They work together, across pages.

CLI

shiftlabs targets lists, reads, creates and updates targets. Creating and updating need an admin key.

shellterminal
npx shiftlabs targets create core-sw-01 --type device --name "Core switch 1" \
  --attr site=dc-east --attr vendor=arista --group dc-east --group switches
# target core-sw-01 created (version 1)

npx shiftlabs targets update core-sw-01 --attr site=dc-east --attr vendor=arista --attr rack=r12 --if-match 1
# target core-sw-01 at version 2

npx shiftlabs targets list --group dc-east
# core-sw-01  device  Core switch 1  dc-east,switches
  • list

    targets list prints a page, 50 by default, with --type, --group and --limit up to 100. When more remain, its last line is the command for the next page, with --before.

  • get

    targets get <id> prints the target's version, when it changed last, its groups, its HA group and each attribute.

  • create and update

    --type, --name, --attr <key>=<value> and --group as often as needed, and --ha-group. targets update sends only the options given, each replacing that field whole: its --attr options are all the target's attributes, its --group options all its groups, and --ha-group "" clears the HA group.

  • Versions

    targets update --if-match <version> changes the target only while it's at that version. Otherwise, and for an id the workspace already has, the command exits 7 (conflict).

  • JSON

    With --json, get, create and update print the target, and list prints {"targets", "nextBefore"}. See CLI.

Client

The client in TypeScript and Python has the same four calls.

server.ts
import { Shiftlabs } from "shiftlabs/client";

const sl = new Shiftlabs({ apiKey: process.env.SHIFTLABS_API_KEY! });

await sl.targets.create({ id: "core-sw-01", type: "device", name: "Core switch 1", groups: ["dc-east"] });
const target = await sl.targets.get("core-sw-01");
await sl.targets.update("core-sw-01", { attributes: { ...target.attributes, rack: "r12" } }, { ifMatch: target.version });

const { targets, nextBefore } = await sl.targets.list({ type: "device", group: "dc-east" });
  • Calls

    targets.list({ type, group, limit, before }) returns {targets, nextBefore}; Python's list(type=, group=, limit=, before=) returns TargetList(targets, next_before). targets.get(id), targets.create(target) and targets.update(id, fields, { ifMatch }), or if_match= in Python, return the target.

  • Conflicts

    A stale ifMatch and an id the workspace already has throw conflict. The error's response.target is the target as it is now.

  • Keys

    list and get work with any key, viewer up. create and update need an admin key.

History and audit

  • History

    Each change is kept as a revision of its target: who made it, when, and the target before and after. Revisions stay as long as the target, past the audit log's 90 days.

  • Audit log

    Each create and update is in the audit log as target_create or target_update, its subject the target's id. Its detail has by, the member's email or the API key's id, the new version, and what changed: after for a create, the changed fields' before and after for an update. An attribute's entry holds only the keys that changed, null on the side it's missing from. A detail is at most 8,192 characters. In a longer one, each attribute value over 256 characters becomes {"cut": true, "start", "chars", "sha256"}: its first 64 characters, its length and the SHA-256 of the whole value, so entries still show whether it changed. If the detail is still too long, each side's attributes, then its groups, become {"cut": true, "chars", "sha256"} of their JSON. The target's revisions keep every value whole.

Who can

  • Read

    Every role, viewer up, lists and reads targets: target.view.

  • Write

    Only admins create and update them: target.manage. Anyone else gets 403 {"error": "forbidden", "required": "admin", "capability": "target.manage"}.

  • Keys

    A key holds the capabilities its role does, or those it lists. A key limited to some agents or workflows can't reach targets.

Routes
RouteMethodMinimum role
/v1/targets ?type=&group=&limit=&before=: newest created first, 50 a page, the next from Next-BeforeGETviewer
/v1/targets/:id the target, its version as ETagGETviewer
/v1/targets {"id", "type", "name", "attributes"?, "groups"?, "haGroup"?}; 409 for an id the workspace has; auditedPOSTadmin
/v1/targets/:id the fields to replace; If-Match: "<version>" answers 412 once it changed; auditedPATCHadmin