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.
{
"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
}
idUnique 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 ofstep.snapshot.typedevice,host,cluster,serviceorcustom.name1 to 255 characters.
attributesKey/value strings, 64 at most: a key 1 to 64 characters, a value up to 1,024. Default
{}.groupsGroup names, 32 at most, each 1 to 64 characters. Each is kept once, and they come back sorted. Default
[].haGroupThe HA group the target is in, up to 64 characters, or
null, the default. It is kept as given; nothing acts on it yet.version1 when the target is created, and one more with each change.
createdAtandupdatedAtare 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.
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/targetsanswers201with the target. An id the workspace already has answers409 {"error": "target already exists", "target"}with that target, and changes nothing. - Update
PATCH /v1/targets/:idtakes the fields to change. Each replaces the target's whole field:attributesis every attribute the target keeps, andgroupsevery group."haGroup": nullclears 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 answers400. To rename a target, change itsname. - Versions
Reads answer the version as
ETag. Send it back asIf-MatchonPATCH: when the target changed since, it answers412 {"error", "target"}with the target as it is now, and changes nothing. WithoutIf-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"},fieldnaming it, and changes nothing. - Ids in paths
Percent-encode the id into one path segment:
core sw/1is/v1/targets/core%20sw%2F1.
List and read
- One target
GET /v1/targets/:idanswers the target, or404 {"error": "target not found"}. - Pages
GET /v1/targetslists the newest created first, 50 a page,?limit=up to 100. While more remain, theNext-Beforeheader 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.
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 listprints a page, 50 by default, with--type,--groupand--limitup 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--groupas often as needed, and--ha-group.targets updatesends only the options given, each replacing that field whole: its--attroptions are all the target's attributes, its--groupoptions 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 exits7(conflict). - JSON
With
--json,get,createandupdateprint the target, andlistprints{"targets", "nextBefore"}. See CLI.
Client
The client in TypeScript and Python has the same four calls.
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" });
import os
from shiftlabs.client import Shiftlabs
sl = Shiftlabs(api_key=os.environ["SHIFTLABS_API_KEY"])
sl.targets.create({"id": "core-sw-01", "type": "device", "name": "Core switch 1", "groups": ["dc-east"]})
target = sl.targets.get("core-sw-01")
sl.targets.update("core-sw-01", {"attributes": {**target["attributes"], "rack": "r12"}}, if_match=target["version"])
page = sl.targets.list(type="device", group="dc-east")
print(page.targets, page.next_before)
- Calls
targets.list({ type, group, limit, before })returns{targets, nextBefore}; Python'slist(type=, group=, limit=, before=)returnsTargetList(targets, next_before).targets.get(id),targets.create(target)andtargets.update(id, fields, { ifMatch }), orif_match=in Python, return the target. - Conflicts
A stale
ifMatchand an id the workspace already has throwconflict. The error'sresponse.targetis the target as it is now. - Keys
listandgetwork with any key, viewer up.createandupdateneed 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_createortarget_update, its subject the target's id. Its detail hasby, the member's email or the API key's id, the newversion, and what changed:afterfor a create, the changed fields'beforeandafterfor an update. An attribute's entry holds only the keys that changed,nullon 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'sattributes, then itsgroups, 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 gets403 {"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.
| Route | Method | Minimum role |
|---|---|---|
/v1/targets ?type=&group=&limit=&before=: newest created first, 50 a page, the next from Next-Before | GET | viewer |
/v1/targets/:id the target, its version as ETag | GET | viewer |
/v1/targets {"id", "type", "name", "attributes"?, "groups"?, "haGroup"?}; 409 for an id the workspace has; audited | POST | admin |
/v1/targets/:id the fields to replace; If-Match: "<version>" answers 412 once it changed; audited | PATCH | admin |