client

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package client provides job client operations for NATS JetStream.

Index

Constants

View Source
const (
	// DefaultPageSize is the default number of jobs per page.
	DefaultPageSize = 10
	// MaxPageSize is the maximum allowed page size.
	MaxPageSize = 100
)

Variables

View Source
var (
	// ErrResponseNotSigned means the response carried no signed envelope.
	// Expected from an agent that has not been upgraded; never accepted
	// while enforcing.
	ErrResponseNotSigned = errors.New("response is not a signed envelope")

	// ErrResponseKeyUnknown means no key is stored for the machine ID the
	// response claims. Expected during a rollout, before that agent
	// re-enrolls.
	ErrResponseKeyUnknown = errors.New("no stored key for responding agent")

	// ErrResponseStoreUnavailable means the store could not be read. It is
	// never mistaken for "no stored key".
	ErrResponseStoreUnavailable = errors.New("agent key store unavailable")

	// ErrResponseSignatureInvalid means a key that is not the stored key
	// for that agent signed the response.
	ErrResponseSignatureInvalid = errors.New("invalid agent signature on response")

	// ErrResponseHostnameMismatch means the signature verified, but the
	// response claims a hostname the signing agent did not enroll under —
	// an accepted agent answering for a host that is not its own.
	ErrResponseHostnameMismatch = errors.New(
		"response hostname does not match the enrolled hostname",
	)
)

The causes of a rejected response stay distinct so an operator can tell an agent that has not re-enrolled yet from one whose messages are being forged, and both from a store that cannot be read. None of them ever means "treat as verified".

Functions

func ComputeAgentState

func ComputeAgentState(
	events []job.TimelineEvent,
) string

ComputeAgentState returns the current state from timeline events.

Types

type AgentKey

type AgentKey struct {
	// MachineID is the permanent host identifier the record is keyed by.
	MachineID string
	// Hostname is the hostname recorded at acceptance. A response claiming
	// a different hostname is not this agent's to answer for.
	Hostname string
	// PublicKey is the key recorded at acceptance.
	PublicKey ed25519.PublicKey
	// SupersededKey is the key a rotation replaced, kept acceptable until
	// SupersededUntil so a message signed moments before the rotation is not
	// rejected for arriving late. Absent outside a rotation window.
	SupersededKey ed25519.PublicKey
	// SupersededUntil is the instant the superseded key stops being accepted.
	SupersededUntil time.Time
}

AgentKey is what the controller holds for an accepted agent: the key its messages are verified against, and the hostname it enrolled under.

func (*AgentKey) AcceptableKeys

func (k *AgentKey) AcceptableKeys(
	now time.Time,
) []ed25519.PublicKey

AcceptableKeys returns the keys a message from this agent may be signed with: the current one, and the key a rotation replaced while it is still inside its grace period.

An expired superseded key is simply not returned, so a signature matching only it fails as a mismatch rather than as its own outcome. That is deliberate: after the window, that key is no longer this agent's.

type AgentKeyStore

type AgentKeyStore interface {
	LookupAgentKey(
		ctx context.Context,
		machineID string,
	) (*AgentKey, error)
}

AgentKeyStore looks up an accepted agent's key by machine ID. Nil when the controller is not enforcing response verification, which leaves behaviour exactly as it was before verification existed.

Implemented on the controller by the enrollment watcher, which is the only thing allowed to write the underlying records.

type Client

type Client struct {

	// JSONMarshalFn is the function used to marshal JSON. Defaults to json.Marshal.
	// Exported for testing.
	JSONMarshalFn func(v any) ([]byte, error)
	// contains filtered or unexported fields
}

Client provides methods for publishing job requests and retrieving responses.

func New

func New(
	logger *slog.Logger,
	natsClient NATSClient,
	opts *Options,
) (*Client, error)

New creates a new jobs client using an existing NATS client.

func (*Client) CheckDrainFlag

func (c *Client) CheckDrainFlag(
	ctx context.Context,
	machineID string,
) bool

CheckDrainFlag returns true if the drain flag exists for the given identifier. Callers pass a machine ID (hex string, no sanitization needed).

func (*Client) ConsumeJobs

func (c *Client) ConsumeJobs(
	ctx context.Context,
	streamName, consumerName string,
	handler func(jetstream.Msg) error,
	opts *natsclient.ConsumeOptions,
) error

ConsumeJobs sets up message consumption for job processing.

func (*Client) CreateJob

func (c *Client) CreateJob(
	ctx context.Context,
	operationData map[string]interface{},
	targetHostname string,
) (*CreateJobResult, error)

CreateJob creates a new job from operation data and stores it in the KV bucket.

func (*Client) CreateOrUpdateConsumer

func (c *Client) CreateOrUpdateConsumer(
	ctx context.Context,
	streamName string,
	consumerConfig jetstream.ConsumerConfig,
) error

CreateOrUpdateConsumer creates or updates a JetStream consumer.

func (*Client) DeleteDrainFlag

func (c *Client) DeleteDrainFlag(
	ctx context.Context,
	machineID string,
) error

DeleteDrainFlag removes the drain flag for an agent from the state KV bucket. The agent detects this on heartbeat and resumes accepting jobs. Callers pass a machine ID (hex string, no sanitization needed).

func (*Client) DeleteJob

func (c *Client) DeleteJob(
	ctx context.Context,
	jobID string,
) error

DeleteJob deletes a job from the KV store by its ID.

func (*Client) GetAgent

func (c *Client) GetAgent(
	ctx context.Context,
	target string,
) (*job.AgentInfo, error)

GetAgent reads a single agent's registration from the KV registry. It tries a direct machine ID lookup first, then falls back to scanning all agents by hostname.

func (*Client) GetAgentTimeline

func (c *Client) GetAgentTimeline(
	ctx context.Context,
	hostname string,
) ([]job.TimelineEvent, error)

GetAgentTimeline returns sorted timeline events for a hostname.

func (*Client) GetJobData

func (c *Client) GetJobData(
	ctx context.Context,
	jobKey string,
) ([]byte, error)

GetJobData retrieves raw job data from the KV store.

func (*Client) GetJobStatus

func (c *Client) GetJobStatus(
	ctx context.Context,
	jobID string,
) (*job.QueuedJob, error)

GetJobStatus returns information about a specific job.

func (*Client) GetQueueSummary

func (c *Client) GetQueueSummary(
	ctx context.Context,
) (*job.QueueStats, error)

GetQueueSummary returns job queue statistics derived from KV key names only — no entry reads.

func (*Client) HasJobResponse

func (c *Client) HasJobResponse(
	ctx context.Context,
	jobID string,
	hostname string,
) (bool, error)

HasJobResponse reports whether this agent has already written a response for the given job. A redelivered message must not re-execute an operation this agent has already answered, so the handler checks this before running the operation. The check is scoped to hostname so broadcast jobs, where every targeted agent responds independently, remain unaffected.

func (*Client) ListAgents

func (c *Client) ListAgents(
	ctx context.Context,
) ([]job.AgentInfo, error)

ListAgents reads the agent registry KV bucket and returns all registered agents. Agents register via heartbeat, so only live agents appear.

func (*Client) ListJobs

func (c *Client) ListJobs(
	ctx context.Context,
	statusFilter string,
	limit int,
	offset int,
) (*ListJobsResult, error)

ListJobs returns jobs filtered by status with server-side pagination. Uses a two-pass approach: Pass 1 derives status from key names only (fast), Pass 2 fetches full details for the paginated page only. Jobs are returned newest-first (reverse insertion order).

func (*Client) Modify

func (c *Client) Modify(
	ctx context.Context,
	target string,
	category string,
	operation job.OperationType,
	data any,
) (string, *job.Response, error)

Modify publishes a modify job to a single target and waits for the response.

func (*Client) ModifyBroadcast

func (c *Client) ModifyBroadcast(
	ctx context.Context,
	target string,
	category string,
	operation job.OperationType,
	data any,
) (string, map[string]*job.Response, error)

ModifyBroadcast publishes a modify job to a broadcast target and collects all responses.

func (*Client) Query

func (c *Client) Query(
	ctx context.Context,
	target string,
	category string,
	operation job.OperationType,
	data any,
) (string, *job.Response, error)

Query publishes a query job to a single target and waits for the response.

func (*Client) QueryBroadcast

func (c *Client) QueryBroadcast(
	ctx context.Context,
	target string,
	category string,
	operation job.OperationType,
	data any,
) (string, map[string]*job.Response, error)

QueryBroadcast publishes a query job to a broadcast target and collects all responses.

func (*Client) RetryJob

func (c *Client) RetryJob(
	ctx context.Context,
	jobID string,
	targetHostname string,
) (*CreateJobResult, error)

RetryJob creates a new job using the same operation data as an existing job. The original job is preserved. A "retried" status event is written to the original job's timeline linking to the new job.

func (*Client) SetAgentKeyStore

func (c *Client) SetAgentKeyStore(
	store AgentKeyStore,
)

SetAgentKeyStore wires the store used to verify agent responses. See the JobClient interface for why this is separate from construction. Passing nil leaves responses unverified.

func (*Client) SetDrainFlag

func (c *Client) SetDrainFlag(
	ctx context.Context,
	machineID string,
) error

SetDrainFlag writes the drain flag for an agent in the state KV bucket. The agent detects this flag on heartbeat and stops accepting jobs. Callers pass a machine ID (hex string, no sanitization needed).

func (*Client) SetMachineID

func (c *Client) SetMachineID(
	machineID string,
)

SetMachineID records the identity stamped on payloads this client signs.

func (*Client) SetMeterProvider

func (c *Client) SetMeterProvider(
	mp *sdkmetric.MeterProvider,
)

SetMeterProvider creates OTEL instruments for job metrics.

func (*Client) SetPKISigner

func (c *Client) SetPKISigner(
	signer PKISigner,
)

SetPKISigner wires the signer used to sign outgoing job and response payloads and to verify incoming ones. See the JobClient interface for why this is separate from construction. Passing nil disables signing and verification.

func (*Client) WriteAgentTimelineEvent

func (c *Client) WriteAgentTimelineEvent(
	ctx context.Context,
	hostname, event, message string,
) error

WriteAgentTimelineEvent writes an append-only timeline event for an agent state transition.

func (*Client) WriteJobResponse

func (c *Client) WriteJobResponse(
	_ context.Context,
	jobID, hostname string,
	responseData []byte,
	status string,
	errorMsg string,
	changed *bool,
) error

WriteJobResponse stores agent response data for a job.

func (*Client) WriteStatusEvent

func (c *Client) WriteStatusEvent(
	_ context.Context,
	jobID, event, hostname string,
	data map[string]interface{},
) error

WriteStatusEvent writes an append-only status event for a job. This eliminates race conditions by never updating existing keys.

type CreateJobResult

type CreateJobResult struct {
	JobID     string `json:"job_id"`
	Status    string `json:"status"`
	Revision  uint64 `json:"revision"`
	Timestamp string `json:"timestamp"`
}

CreateJobResult represents the result of creating a job.

type JobClient

type JobClient interface {
	// Generic dispatch operations — used by API handlers to submit jobs
	// without importing typed wrapper methods.
	Query(
		ctx context.Context,
		target string,
		category string,
		operation job.OperationType,
		data any,
	) (string, *job.Response, error)
	QueryBroadcast(
		ctx context.Context,
		target string,
		category string,
		operation job.OperationType,
		data any,
	) (string, map[string]*job.Response, error)
	Modify(
		ctx context.Context,
		target string,
		category string,
		operation job.OperationType,
		data any,
	) (string, *job.Response, error)
	ModifyBroadcast(
		ctx context.Context,
		target string,
		category string,
		operation job.OperationType,
		data any,
	) (string, map[string]*job.Response, error)

	// Job queue management operations
	GetQueueSummary(
		ctx context.Context,
	) (*job.QueueStats, error)
	GetJobStatus(
		ctx context.Context,
		jobID string,
	) (*job.QueuedJob, error)
	ListJobs(
		ctx context.Context,
		statusFilter string,
		limit int,
		offset int,
	) (*ListJobsResult, error)

	// Agent discovery
	ListAgents(
		ctx context.Context,
	) ([]job.AgentInfo, error)
	GetAgent(
		ctx context.Context,
		hostname string,
	) (*job.AgentInfo, error)

	// Agent timeline
	WriteAgentTimelineEvent(
		ctx context.Context,
		hostname, event, message string,
	) error
	GetAgentTimeline(
		ctx context.Context,
		hostname string,
	) ([]job.TimelineEvent, error)

	// Agent drain flag
	CheckDrainFlag(
		ctx context.Context,
		hostname string,
	) bool
	SetDrainFlag(
		ctx context.Context,
		hostname string,
	) error
	DeleteDrainFlag(
		ctx context.Context,
		hostname string,
	) error

	// Job deletion
	DeleteJob(
		ctx context.Context,
		jobID string,
	) error

	// Job retry
	RetryJob(
		ctx context.Context,
		jobID string,
		targetHostname string,
	) (*CreateJobResult, error)

	// Agent operations - used by agents for processing
	// HasJobResponse reports whether this agent has already recorded a
	// response for the given job. Used to detect a redelivered message
	// for a job this agent has already executed, so the operation is not
	// run a second time.
	HasJobResponse(
		ctx context.Context,
		jobID string,
		hostname string,
	) (bool, error)
	WriteStatusEvent(
		ctx context.Context,
		jobID string,
		event string,
		hostname string,
		data map[string]interface{},
	) error
	WriteJobResponse(
		ctx context.Context,
		jobID string,
		hostname string,
		responseData []byte,
		status string,
		errorMsg string,
		changed *bool,
	) error
	ConsumeJobs(
		ctx context.Context,
		streamName string,
		consumerName string,
		handler func(jetstream.Msg) error,
		opts *natsclient.ConsumeOptions,
	) error
	GetJobData(
		ctx context.Context,
		jobKey string,
	) ([]byte, error)
	CreateOrUpdateConsumer(
		ctx context.Context,
		streamName string,
		consumerConfig jetstream.ConsumerConfig,
	) error

	// SetPKISigner wires the signer used to sign outgoing job and response
	// payloads and to verify incoming ones. The keypair a signer needs is
	// not always available at client construction time, so this is called
	// once one is: on the controller when the enrollment watcher loads the
	// controller keypair, and on the agent when PKI enrollment loads the
	// agent keypair. Passing nil disables signing and verification.
	SetPKISigner(
		signer PKISigner,
	)

	// SetAgentKeyStore wires the store used to verify agent responses. Like
	// the signer, it is not available at construction: on the controller it
	// arrives with the enrollment watcher. Passing nil leaves responses
	// unverified, which is the behaviour before this feature.
	SetAgentKeyStore(
		store AgentKeyStore,
	)

	// SetMachineID records the identity stamped on payloads this client
	// signs, so the verifier knows which stored key to check them against.
	// Set on the agent once its identity is resolved; empty on the
	// controller, whose payloads agents verify against the controller key.
	SetMachineID(
		machineID string,
	)
}

JobClient defines the interface for interacting with the jobs system.

type ListJobsResult

type ListJobsResult struct {
	Jobs         []*job.QueuedJob
	TotalCount   int
	StatusCounts map[string]int
}

ListJobsResult represents the result of listing jobs with pagination.

type NATSClient

type NATSClient interface {
	Publish(
		ctx context.Context,
		subject string,
		data []byte,
	) error
	GetStreamInfo(
		ctx context.Context,
		streamName string,
	) (*jetstream.StreamInfo, error)
	KVPut(
		bucket string,
		key string,
		value []byte,
	) error
	ConsumeMessages(
		ctx context.Context,
		streamName string,
		consumerName string,
		handler natsclient.JetStreamMessageHandler,
		opts *natsclient.ConsumeOptions,
	) error
	CreateOrUpdateConsumerWithConfig(
		ctx context.Context,
		streamName string,
		consumerConfig jetstream.ConsumerConfig,
	) error
}

NATSClient defines the NATS operations needed by the job client.

type Options

type Options struct {
	// Timeout for waiting for job responses (default: 30s)
	Timeout time.Duration
	// KVBucket for job storage (required)
	KVBucket jetstream.KeyValue
	// RegistryKV is the KV bucket for agent registry (optional).
	RegistryKV jetstream.KeyValue
	// FactsKV is the KV bucket for agent facts (optional).
	FactsKV jetstream.KeyValue
	// StateKV is the KV bucket for persistent agent state (drain flags, timeline).
	StateKV jetstream.KeyValue
	// StreamName is the JetStream stream name (used to derive DLQ name).
	StreamName string
	// PKISigner signs job payloads when PKI is enabled. Nil when disabled.
	PKISigner PKISigner
	// TargetResolver resolves hostname targets to machine IDs for NATS
	// subject routing. When nil, targets are used as-is.
	TargetResolver func(string) string
}

Options configures the jobs client.

type PKISigner

type PKISigner interface {
	// Sign signs the given data with the signer's private key.
	Sign(data []byte) []byte
	// Fingerprint returns the SHA256 fingerprint of the signer's public key.
	Fingerprint() string
	// ControllerPublicKey returns the controller's public key for verifying
	// controller-signed messages. Returns nil if not set.
	ControllerPublicKey() ed25519.PublicKey
}

PKISigner signs and verifies payloads. Nil when PKI is disabled.

Jump to

Keyboard shortcuts

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