fleetreg

package
v0.14.29-dev Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package fleetreg pushes this host's fleet inventory — the tools, agents, and skills it has installed — up to cloudbox.

It is the counterpart of internal/agent/ollama's model-registry watcher, and deliberately the same shape: the host is the source of truth about itself, cloudbox caches that truth behind a freshness clock, and a content hash lets an unchanged snapshot cost one UPDATE instead of a rewrite.

The inventory is read straight out of coreutils/pkg/fleet rather than by shelling out to `bashy`. Outpost already depends on coreutils, so the registry is a library call — and a host that has no bashy binary installed still has a fleet, because the compiled-in baseline is part of the library.

Index

Constants

View Source
const (
	// PlacementSelf — this host hosts the apiserver.
	PlacementSelf = "self"
	// PlacementPeer — a control plane on another of the user's own machines,
	// reached through a tunnel that lands on loopback.
	PlacementPeer = "peer"
	// PlacementCloudbox — the hosted control plane.
	PlacementCloudbox = "cloudbox"
	// PlacementExternal — anything else: a rented always-on box, a managed
	// provider cluster.
	PlacementExternal = "external"
)

Placement names WHERE the control plane this host joined actually runs.

It exists because placement became a user choice (dhnt/docs/dks-control-plane-on-sphere.md): the same host, running the same agent, may be attached to a cloudbox-hosted plane today and a peer-hosted one tomorrow. Reporting it is what makes that move VISIBLE — otherwise a node silently changes which cluster it belongs to and the only way to find out is to go read its config.

Variables

This section is empty.

Functions

func ClassifyPlacement

func ClassifyPlacement(apiURL, cloudboxURL string, controlPlane bool) string

ClassifyPlacement decides where the joined control plane runs.

Order matters: the control-plane host's own endpoint is loopback too, so the ControlPlane flag has to be consulted before the loopback test or every control plane would report itself as a peer.

KNOWN LIMIT, stated rather than papered over: a host that joins a k3s it runs itself but never sets control_plane reports `peer`. The cluster IDENTITY is still correct (it comes from the CA), so grouping holds — only the placement label is wrong, and it is wrong because the config omitted the fact.

func ContentHash

func ContentHash(assets []Asset) string

ContentHash is a deterministic sha256 over the reported inventory.

The push order cannot change it (assets are sorted), and no timestamp enters it — the whole point is that an unchanged host hashes the same across polls, so cloudbox can skip the rewrite and just move the freshness clock.

Types

type Asset

type Asset struct {
	Kind    string `json:"kind"`
	Name    string `json:"name"`
	Display string `json:"display,omitempty"`
	Detail  string `json:"detail,omitempty"`
}

Asset is one reported tool / agent / skill.

type ClusterInfo

type ClusterInfo struct {
	// Placement is one of the constants above.
	Placement string `json:"placement"`
	// Endpoint is the apiserver URL as THIS host reaches it. For a peer-hosted
	// plane that is a loopback address whose port is a local tunnel artifact —
	// meaningful for debugging this host, meaningless as a cluster identity,
	// which is precisely why it is not the identity.
	Endpoint string `json:"endpoint,omitempty"`
	// ControlPlane reports whether this host runs the apiserver.
	ControlPlane bool `json:"control_plane,omitempty"`
	// Nodes are the Kubernetes node names this host registers. One host owns
	// many nodes: one k3s agent plus one per virtual-kubelet backend.
	Nodes []string `json:"nodes,omitempty"`
	// Runtimes are the enabled backends (agent, vk-native, vk-podman, …).
	Runtimes []string `json:"runtimes,omitempty"`

	// HostsControlPlane reports that this host RUNS a control plane. It is a
	// fact about the host, deliberately separate from the row's own cluster:
	// hosting and joining became independent decisions, so a host can run a
	// plane for others while being a node of somebody else's.
	//
	// ControlPlane above stays the narrower claim — "this host hosts THE
	// CLUSTER IN THIS ROW" — because that is what the inventory needs to name
	// a cluster's control-plane member.
	HostsControlPlane bool `json:"hosts_control_plane,omitempty"`
	// ControlPlaneEndpoint is where that hosted plane's tunnel listens, when
	// this host runs one.
	ControlPlaneEndpoint string `json:"control_plane_endpoint,omitempty"`

	// CA is the cluster's TLS CA bundle. Never serialized — it is read only to
	// derive the cluster identity below.
	CA []byte `json:"-"`
	// NodeToken is the k3s join token. Never serialized — it is a CREDENTIAL,
	// read only for the CA hash it embeds.
	NodeToken string `json:"-"`
}

ClusterInfo is what a host reports about its Kubernetes participation.

It is deliberately a REPORT, not a definition: an outpost may say which cluster it is in, it may not author the cluster. That is the same report/author split the *:registry vs *:write scopes encode, and it is why this rides the existing fleet-registry push instead of getting an endpoint that could be mistaken for a control API.

func (*ClusterInfo) ClusterID

func (c *ClusterInfo) ClusterID() string

ClusterID is the stable identity of the cluster this host joined.

IT IS THE CA FINGERPRINT, NOT THE URL, and that choice is load-bearing. Every member of a cluster trusts the SAME apiserver CA, but members do not agree on a URL: a node reaching a peer-hosted plane through a tunnel sees `https://127.0.0.1:<locally-derived-port>`, and two nodes on one cluster derive DIFFERENT ports. Keying on the URL would split one cluster into one pseudo-cluster per node — the inventory would report N clusters of one node each and the grouping the page exists for would be silently wrong.

The CA is a public certificate, so hashing it leaks nothing; the hash is used only so the identity is short and fixed-width.

Falling back to the URL host is correct for the case that produces an empty CA — cloudbox fronting the apiserver behind a real, publicly-trusted cert — because there every member DOES agree on the URL. A JOIN TOKEN ALREADY CARRIES THE CLUSTER'S CA HASH, and it is the right source when joining a peer plane. k3s node tokens are `K10<sha256-of-cluster-CA>::<user>:<secret>`, so the identity is available even though the peer's CA bundle itself is not — a worker joining someone else's plane holds a token but no CA (its ClusterConfig.CA belongs to the cloudbox pairing, a DIFFERENT cluster). Reading it here is what stops a peer-joined host from being grouped under the cloudbox cluster whose CA it happens to be carrying.

type Config

type Config struct {
	// CloudboxURL is the base URL (e.g. https://ai.dhnt.io).
	CloudboxURL string
	// AccessToken is the per-outpost bearer JWT. It must carry
	// fleet:registry — the scope minted for a HOST reporting a fact about
	// itself, not for a user authoring a definition.
	AccessToken string
	// AgentName is the host name cloudbox knows this outpost by.
	AgentName string

	PollInterval      time.Duration
	HeartbeatInterval time.Duration
	HTTP              *http.Client

	// Catalog is indirected for tests. Nil uses the host's real registry.
	Catalog func() *fleet.Catalog
	// Skills lists the skills installed in the host's local store. Nil uses
	// the default reader.
	//
	// It reads directory names rather than importing coreutils/pkg/skills:
	// that package pulls the dhnt skill-CNL runtime, and outpost is the lean
	// mesh supervisor. A name is all the inventory needs — a peer asking
	// "does host-a have the conductor skill" wants a yes, not the procedure.
	Skills func() []string

	// Cluster reports this host's Kubernetes participation, or nil when it
	// joins none. Indirected like Catalog/Skills so the watcher stays free of
	// conf coupling and testable without a config file.
	Cluster func() *ClusterInfo
}

Config configures the watcher.

type PushError

type PushError struct {
	Status int
	Body   string
}

PushError carries the HTTP status so the caller can honor the endpoint's contract: 401 means stop (revoked or unknown token), 403 means the token lacks fleet:registry, 404 means this host is not paired under that owner. Only 5xx is worth retrying.

func (*PushError) Error

func (e *PushError) Error() string

func (*PushError) Fatal

func (e *PushError) Fatal() bool

Fatal reports whether retrying can never help.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher polls the local fleet registry and pushes changes.

func New

func New(cfg Config) (*Watcher, error)

New validates cfg and constructs a Watcher.

func (*Watcher) Push

func (w *Watcher) Push(ctx context.Context, assets []Asset, contentHash string) (string, error)

Push sends one snapshot. It returns the server's `applied` verdict ("replaced" or "touched") so a caller can log what actually happened.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context)

Run polls until ctx is cancelled, pushing when the inventory changes and on a heartbeat so the freshness clock keeps ticking.

A failed push is logged and retried on the next tick; it never takes the daemon down. The inventory is a convenience for peers, not a dependency of anything running on this host.

func (*Watcher) Snapshot

func (w *Watcher) Snapshot() []Asset

Snapshot reads the host's current inventory.

Tools are the agentic-CLI kind only. A function kit is not something this host can be asked to launch, so reporting one would advertise a capability that does not exist.

Jump to

Keyboard shortcuts

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