Help us shape the future of Go! Take the 2026 Go Developer Survey

inventory

package
v0.45.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0, Apache-2.0 Imports: 15 Imported by: 0

README

Managed inventory providers

Use factserver.InventoryHandler(data, caPublicKey, controlPublicKey, factserver.WithInventoryManagement(management)) to serve facts and the inventory management API. data implements inventory.Hosts; management implements inventory.Store. The interfaces are independent: use the same instance twice or separate implementations describing the same logical inventory. Omitting the option serves facts only, even if data also implements management. The executable opens its store, owns its lifetime and listener, and configures the trusted keys. An empty control key turns off management. The shared packages have no cloud SDK dependencies.

Store exposes complete operations rather than transactions or database rows. An adapter implements its database schema, queries, conditional writes and transactions. Every mutation commits records, token state, and audit together. Run storetest.Run with one fresh inventory to exercise the contract against another backend. The factory returns one storetest.Backend{Hosts: data, Store: management} value pairing the independent data and management views. The factory owns its cleanup.

Resource versioning is optional. SupportsVersioning() declares whether the store returns nonempty Host.Version values. Versions are opaque strings; callers compare them only for equality and pass them back as expectedVersion in UpsertHost write conditions and to RemoveHost. An empty expected version requests no version check. Stores without versioning return empty versions and callers pass empty preconditions. Enforcement and atomicity are backend properties. SQLite supports versioning and keeps its numeric counter entirely inside the adapter.

Host contains names, authorization facts, and enrollment metadata. Names may be exact hostnames or wildcard expressions in the same collection. There is no separate pattern resource or proposal object. Enroll accepts exact names and creates pending state (or redeems a token). All creation and updates use UpsertHost; reads and removal use GetHost, ListHosts, and RemoveHost. Administrator creation may include exact or wildcard names.

Storage owns the distinction between exact and wildcard names. SQLite indexes that distinction per name: lookup checks exact names first, then reads only wildcard names when there is no active exact match. A wildcard match supplies the requested hostname and the matching record's facts, without assigning that record's management identity to the synthesized host. Multiple matching names on one record are unambiguous; matching different records is a conflict.

Epithet generates IDs with inventory.NewID before calling storage. Token credentials become the enrolled host ID. Tokens do not create host reservations. Each mutation receives an AuditEvent containing the authenticated actor, action, resource ID and operation timestamp. Storage preserves every field; it does not construct or complete audit events. Record timestamps use the supplied event time. Removal retains audit history and appends the supplied removal event.

UpsertHost(ctx, host, conditions, event) addresses host.ID. Its WriteConditions carries an optional ExpectedVersion, CreateOnly to forbid replacing existing facts, and ConsumeToken to require the single-use credential matching the host ID. The zero value permits an ordinary upsert. Enrollment sets CreateOnly and, for preapproval, ConsumeToken; identical retries return the stored host without changing its status or audit history. Administrator writes use the optional version condition. Validation and review policy stay above storage; the backend applies these write conditions with persistence.

SQLite uses schema version 5, with indexed exact/wildcard names, retained audit history, and no inventory-wide revision counter. Older schema versions are rejected without modification; there is no automatic migration.

Epithet owns enrollment and review policy:

  • Enroll validates the submitted facts and creates a pending host, or calls UpsertHost to create the host and consume its token in one storage operation. SQLite commits activation, token consumption, and the supplied audit event in one transaction; a failed operation leaves the token usable.
  • Review validates administrator edits and approval/denial transitions before storage, and selects the audit action.
  • Creation and Replacement prepare records for persistence. Replacement checks metadata and an optional expected version; it does not impose the administrator review workflow on storage.
  • NewAuditEvent constructs events before storage. Each mutation persists the supplied event, including token activation, without a separate redemption event.

Adapters enforce active exact-name/pattern uniqueness. Realms may be shared across records with independent labels and account restrictions, including generated realms. Pending and denied records do not reserve names. Enrollment retries with the same ID and facts return the existing record unchanged; different facts cannot overwrite it. Administrator edits use the same UpsertHost operation.

Lookups return host facts without an inventory-wide revision. Exact names win over patterns; multiple matching active patterns are a conflict. Pattern facts use the requested hostname. Nil accounts mean unrestricted; an empty slice means no accounts. Preserve this distinction in storage and detached results.

HTTP contract

Control discovers management at the complete configured inventory fact URL using a signed OPTIONS. Built-in HTTP providers use /lookup; Unix socket configuration uses that built-in path automatically. Any discovery 4xx means lookup-only support. Successful OPTIONS with no inventory relations also means lookup-only. Transport failures, redirects, 5xx, malformed templates or partial inventory relation sets are errors, never fallback. Discovery is not cached.

HTTP/1.1 204 No Content
Allow: GET, OPTIONS
Cache-Control: no-store
Link-Template: "/inventory/records{/id}{?after,limit,pending}"; rel="https://epithet.dev/rel/inventory-records"
Link-Template: "/inventory/tokens{?token}"; rel="https://epithet.dev/rel/inventory-tokens"
Link-Template: "/inventory/audit{?cursor,limit}"; rel="https://epithet.dev/rel/inventory-audit"

These paths belong to the provider. Clients expand RFC 6570 templates and resolve relative results against the discovery URL. No resource-level OPTIONS is needed. RFC 9652 defines Link-Template.

Template / target Methods Result
records, no id GET 200, JSON array of records
records, no id POST administrator creation, 201
records, id PUT without actor enrollment or identical retry, 200
records, id GET, PUT 200, complete record plus ETag
records, id DELETE 204
tokens, no token POST 201, new token and expiry
tokens, token DELETE 204
audit GET 200, audit page with events and optional next

There is no token retrieval. Record list uses after (exclusive record ID), limit (0/default 100, max 1000), and pending. Audit uses an opaque cursor and limit. Empty cursor starts a read; pass the returned next back unchanged. Empty next means no further page. Ordering, consistency during writes, and cursor lifetime are backend properties. Event timestamps need not be unique or monotonic. The CLI accepts --cursor and prints the page including next. List entries carry no version; GET the selected item before review.

GET and PUT share this representation:

{
  "id": "record-id",
  "status": "pending",
  "host": {
    "name": ["web.example"],
    "attributes": {"environment": "production"},
    "accounts": ["deploy"],
    "principal": {"mode": "epithet-principal-v1", "realm": "production-web"}
  },
  "createdAt": "2026-10-14T09:00:00Z",
  "updatedAt": "2026-10-14T09:00:00Z"
}

Exact and wildcard names use the same name array. Hosts must include accounts. Attributes remain string-valued in managed inventory. Versioned stores return a strong ETag, for example "backend:opaque/version". Administrator PUT and DELETE then require the exact reviewed tag in If-Match; missing preconditions return 428, malformed tags 400, and stale tags 412. Stores without versioning omit ETag and accept writes without If-Match; an explicit version precondition against such a store returns 412. The signed service JWT's if_match claim binds this header to the request.

Control exposes the unquoted ETag value as an optional version string on host records. Mutation requests carry expected-version. Control preserves the token without interpreting it numerically; an absent provider ETag yields no version and no outgoing If-Match.

Administrator PUT echoes id and timestamps unchanged. It either replaces host attributes while retaining status or changes a pending record to active/denied without modifying host attributes. Combined edit and review is rejected. A successful mutation returns the committed record and a new ETag when versioning is supported. Name/pattern conflicts return 409.

Enrollment uses PUT to the records URL with the client-selected id. It carries {"host": {...}} and, for preapproval, "token": "credential" equal to that ID. Control signs the request without an administrator actor. Pending submissions require the shared enrollment bearer at control; it never reaches inventory. Preapproved enrollment validates and consumes the token while creating an active host. Both paths accept identical retries without another write or audit event. Enrollment accepts exact names only and does not use If-Match. It cannot edit an existing host or undo an administrator's review.

Administrator POST to the collection accepts {"host": {...}} and creates an active host with exact or wildcard names. Responses include the record, ETag when supported, and Location.

POST tokens accepts {"lifetimeSeconds": 3600} (0 defaults to one hour; maximum 24 hours), returning {"token": "credential", "expiresAt": "..."}. DELETE expands the known credential into the template's token variable. Invalid, expired or revoked credentials cannot create a host (403). A consumed credential only permits an identical retry of its existing host. Missing resources return 404; storage failures return 503. Errors use {"error": "message"}.

Authentication and CLI

Providers accept request-bound service JWTs signed by control, with audience epithet-inventory. The signed subject carries the already authorized actor; CA reader credentials cannot mutate. Control owns OIDC and the inventory-admin grant. Human CLI operations continue through the agent.

Pending host enrollment requires the control plane's shared bearer credential, configured with --enrollment-token-file FILE or --enrollment-token SECRET. Without it, pending enrollment is disabled. This credential permits only submitting pending hosts; it grants no inventory reads, edits, approval, token management, or SCIM access. Control validates it and sends the ordinary signed enrollment request to the provider. The shared secret never reaches inventory or its storage adapter.

Configure the control service (including when launched by epithet server) with:

enrollment-token-file = "/etc/epithet/enrollment-token"

The public client sends PUT to the control URL followed by the host ID. Host enrollment persists that ID in an id file beside realm and the CA public key before submitting. Pending enrollment generates it once; preapproval uses the single-use credential. Rerunning enrollment reuses the stored ID.

Supply the same secret on an enrolling host:

epithet host enroll --ca https://ca.example/ \
  --enrollment-token-file /path/to/enrollment-token

The client sends this secret in Authorization: Bearer ... to control. The host remains pending until an administrator approves it. The existing --token and --token-file options carry a single-use preapproval credential instead; that path does not require the shared pending-enrollment secret.

Control may configure --inventory-token-file FILE (or --inventory-token) for automation. This is separate from the SCIM credential and enrollment tokens. It grants inventory administration only; audit records identify it as inventory-token. Automation can use:

epithet inventory --control-url https://control.example/manage \
  --admin-token-file /path/to/inventory-admin-token list

The public control/agent command envelope remains the CLI transport; control's provider hop uses the REST contract above. No provider implements that command envelope. inventory token list has been removed. Use token create to receive a credential and token revoke with the known credential to withdraw it.

Documentation

Overview

Package inventory owns managed host facts, enrollment, patterns, and storage.

Index

Constants

View Source
const (
	StatusPending = "pending"
	StatusActive  = "active"
	StatusDenied  = "denied"
)

Host enrollment statuses. Status remains a string in storage and API records.

View Source
const DefaultPageLimit = 100
View Source
const MaxPageLimit = 1000

Variables

View Source
var (
	ErrStorage  = errors.New("managed inventory storage unavailable")
	ErrConflict = errors.New("inventory conflict")
	ErrNotFound = errors.New("inventory record not found")
	ErrToken    = errors.New("invalid, expired, revoked, or used enrollment token")
	ErrVersion  = errors.New("record changed; reload before retrying")
)

Functions

func CheckVersion added in v0.43.0

func CheckVersion(current Host, expectedVersion string) error

CheckVersion compares opaque versions. Empty means no requested version check; the backend determines the consistency of checking and applying a mutation.

func DecodeYAML

func DecodeYAML(data []byte, dst any) error

func NewID added in v0.43.0

func NewID() string

NewID creates an inventory resource ID or single-use enrollment credential.

func SameHostFacts added in v0.43.0

func SameHostFacts(a, b Host) bool

SameHostFacts compares enrollment facts without lifecycle metadata.

Types

type AuditEvent

type AuditEvent struct {
	At       time.Time `yaml:"at"`
	Actor    string    `yaml:"actor"`
	Action   string    `yaml:"action"`
	Resource string    `yaml:"resource"`
}

AuditEvent is supplied by the caller alongside a mutation. Storage preserves every field, including the supplied timestamp.

func NewAuditEvent added in v0.43.0

func NewAuditEvent(actor, action, resource string) AuditEvent

NewAuditEvent describes an operation using the actor authenticated by the caller. The caller selects the resource ID before constructing the event.

type EnrollmentToken

type EnrollmentToken struct {
	ID        string    `yaml:"id"`
	ExpiresAt time.Time `yaml:"expires-at"`
	UsedBy    string    `yaml:"used-by,omitempty"`
	Revoked   bool      `yaml:"revoked"`
}

EnrollmentToken's ID is also its secret and the resulting host ID. Unused tokens retain an empty UsedBy value.

type Host

type Host struct {
	ID            string            `yaml:"-"`
	Version       string            `yaml:"-"`
	CreatedAt     time.Time         `yaml:"-"`
	UpdatedAt     time.Time         `yaml:"-"`
	Status        string            `yaml:"-"`
	Names         []string          `yaml:"names"`
	Labels        map[string]string `yaml:"labels"`
	Accounts      []string          `yaml:"accounts"`
	PrincipalMode PrincipalMode     `yaml:"principal-mode"`
	Realm         principal.Realm   `yaml:"realm,omitempty"`
}

Host contains inventory names, authorization facts, and enrollment metadata. Names may be exact hostnames or wildcard expressions. Lookup prefers exact names; wildcard matches supply facts for the requested hostname. Version is an opaque storage token, empty when the store does not support versioning.

func Creation added in v0.43.0

func Creation(id string, host Host, now time.Time) (Host, error)

Creation prepares storage metadata without deciding the host's status.

func Enroll added in v0.43.0

func Enroll(ctx context.Context, store Store, id string, host Host, token string, event AuditEvent) (*Host, error)

Enroll validates a PUT enrollment before one storage operation. A preapproval token is the host ID; the control-plane pending-enrollment bearer never enters this layer. An identical retry does not update the existing host.

func ParseHost added in v0.43.0

func ParseHost(data []byte) (Host, error)

func Replacement added in v0.43.0

func Replacement(current, proposed Host, expectedVersion string, now time.Time) (Host, error)

Replacement checks resource metadata and prepares a complete host write. Review policy belongs to Review, not this storage operation.

func Review added in v0.43.0

func Review(current, proposed Host, expectedVersion string, now time.Time) (Host, string, error)

Review validates an administrator edit or review at the Epithet boundary. A review cannot also edit the facts being approved. Storage accepts complete replacements, without imposing the administrator review workflow.

func (Host) MarshalYAML added in v0.43.0

func (p Host) MarshalYAML() (any, error)

func (*Host) UnmarshalYAML added in v0.43.0

func (p *Host) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML reads only editable fields; lifecycle metadata is not an input.

func (*Host) Validate added in v0.43.0

func (p *Host) Validate() error

type Hosts

type Hosts interface {
	LookupHost(context.Context, string) (*Host, error)
}

Hosts resolves machines independently of the user directory. LookupHost returns host facts. A missing host returns nil; lookup failures return an error. Read consistency and visibility are backend-specific. Managed lookups prefer exact names over patterns and return ErrConflict for multiple matching active patterns.

type PrincipalMode

type PrincipalMode string

PrincipalMode selects how an allowed account@host tuple is represented in an issued SSH certificate.

const (
	// AccountNamePrincipals places the literal requested account name in the
	// certificate. It is compatible with ordinary OpenSSH CA configuration but
	// is not destination-bound.
	AccountNamePrincipals PrincipalMode = "account-name"

	// EpithetPrincipalV1 derives a destination-bound v1 principal from the
	// principal realm and requested account name.
	EpithetPrincipalV1 PrincipalMode = principal.SchemeV1
)

func (PrincipalMode) Effective

func (m PrincipalMode) Effective() PrincipalMode

Effective returns the concrete mode represented by m.

func (PrincipalMode) Validate

func (m PrincipalMode) Validate() error

Validate rejects unknown non-empty principal modes. Empty is the zero-value spelling of AccountNamePrincipals for compatibility with custom inventory implementations constructed in Go.

type Store

type Store interface {
	// SupportsVersioning is stable for the store's lifetime. When true, returned
	// hosts have nonempty versions that change on mutation. HTTP exposes them as
	// ETags. When false, versions and mutation preconditions are empty.
	SupportsVersioning() bool

	// UpsertHost creates or replaces host.ID, subject to the supplied conditions.
	// Storage owns versions and timestamps; an update retains CreatedAt. The
	// supplied audit event accompanies the write; an unchanged retry appends none.
	// A nonempty expected version cannot match a missing host.
	UpsertHost(ctx context.Context, host Host, conditions WriteConditions, event AuditEvent) (*Host, error)
	// Removal retains audit history and appends the supplied event. A missing ID
	// returns ErrNotFound; a failed version precondition returns ErrVersion.
	RemoveHost(ctx context.Context, id string, expectedVersion string, event AuditEvent) error
	// GetHost returns ErrNotFound for an absent ID.
	GetHost(ctx context.Context, id string) (*Host, error)
	// ListHosts returns hosts ordered by ID, strictly after the cursor.
	// Empty after starts a read; nonempty after is a full resource ID. Zero limit
	// defaults to 100; negative limits and limits above 1,000 are invalid. Pending
	// filters host records before applying the limit. A short page ends that read.
	// Each page is independent; later calls may observe changes.
	ListHosts(ctx context.Context, after string, limit int, pending bool) ([]Host, error)
	// CreateToken stores a single-use credential without creating a host.
	// Its supplied ID becomes the host ID on enrollment. Lifetime must be
	// positive and at most 24 hours.
	CreateToken(ctx context.Context, id string, lifetime time.Duration, event AuditEvent) (EnrollmentToken, error)
	// RevokeToken prevents unused credentials from authorizing enrollment.
	RevokeToken(ctx context.Context, id string, event AuditEvent) error
	// Audit returns events and a backend-owned continuation cursor. Empty input
	// starts a read; empty next completes it. Callers forward cursors unchanged.
	// Ordering, visibility and cursor lifetime belong to the backend. Timestamps
	// need not be unique or monotonic. Zero limit defaults to 100; maximum 1,000.
	Audit(ctx context.Context, cursor string, limit int) (events []AuditEvent, next string, err error)
}

Store is the inventory management contract, independent of Hosts fact lookup. Epithet owns enrollment and review policy; implementations persist host and token operations. Enrollment and any token consumption form one storage call. Consistency is backend-specific.

Only active records contribute lookup facts or reserve authorization names. Active exact names and identical patterns are unique. Overlapping patterns are permitted; ambiguity is detected by Hosts.LookupHost. Admission or edits violating name/pattern uniqueness return ErrConflict. Realms may be shared across records without constraints on their labels or account sets.

Returned records and collections are independent snapshots, including results of mutations. Implementations must not retain or modify caller-owned maps or slices. Nil accounts mean unrestricted; an empty non-nil slice means no accounts. Storage failures return errors matching ErrStorage, never missing records. Validation errors have no sentinel unless a method specifies one below. Contexts bound reads and mutations. The caller owns the store's lifetime; HTTP handlers never close it. Callers construct audit events before storage, supplying the authenticated actor, action, resource and operation timestamp. Storage preserves all event fields. Callers also assign IDs before creation using NewID. Mutation timestamps use event.At. Storage never generates IDs or completes audit events. Version is an opaque, backend-owned token, compared only for equality. Stores without versioning return empty Host.Version and receive empty expectedVersion. Empty expectedVersion means no requested version check; enforcement and consistency are backend properties.

type WriteConditions added in v0.43.0

type WriteConditions struct {
	ExpectedVersion string
	// CreateOnly forbids replacing an existing host. Identical facts return the
	// stored host unchanged, including its status, metadata, and audit history.
	// Different facts return ErrConflict. Enrollment uses this for safe retries.
	CreateOnly bool
	// ConsumeToken requires the single-use token whose secret is host.ID and
	// consumes it with creation. It never authorizes replacement. An identical
	// retry of a host created with that token returns the stored host unchanged;
	// invalid credentials or different facts return ErrToken.
	ConsumeToken bool
}

WriteConditions constrain an upsert without exposing backend transactions. The zero value allows creation or replacement without a version precondition. Epithet checks enrollment and administrator-review policy before storage.

Directories

Path Synopsis
Package sqlitestore implements managed inventory using embedded SQLite.
Package sqlitestore implements managed inventory using embedded SQLite.
Package storetest provides the inventory backend contract suite.
Package storetest provides the inventory backend contract suite.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL