Documentation
¶
Overview ¶
Package fleet is the client side of the cc-backend fleet service. With it a ClusterCockpit service registers with cc-backend, receives its configuration from the central configuration tree, keeps itself marked as alive and learns about the peers it depends on.
The package also holds the wire definitions shared by client and server: the service type codes, the registration request and response bodies, the discovery roster entry, and the line-protocol encoding of heartbeats and rosters. Both sides should use them instead of keeping their own copies.
Lifecycle ¶
A member walks through these steps:
- Register over authenticated REST and receive an instance id.
- Pull the merged configuration. The ETag makes later polls a header-only round trip, and 204 means "no configuration authored", not an error.
- Send a heartbeat periodically, over NATS when a heartbeat subject is configured and the connection is up, and over REST otherwise.
- Optionally, receive discovery rosters on <prefix>.<cluster|infra>.<service type>. Each roster replaces the previous list in full; it is not a delta.
- Deregister on shutdown.
Client.Bootstrap covers steps 1 and 2 at startup and falls back to the optional on-disk cache when cc-backend cannot be reached. Client.Run drives steps 2 to 4, re-registers when cc-backend reports the instance as unknown, and delivers changes on Client.Configs and Client.Rosters. Client.Close stops Run and performs step 5.
Security ¶
The instance id is a bearer credential for heartbeat, config pull and deregistration. The client never logs it and never writes it to the cache file. Registration metadata (Options.Meta) is broadcast unauthenticated in discovery rosters, so it must carry connection hints only, never secrets. The cached configuration may contain secrets, so the cache file is written with mode 0600.
Example ¶
Example shows the lifecycle of a fleet member: bootstrap, run, apply updates from the main loop, close on shutdown.
package main
import (
"context"
"encoding/json"
"errors"
"log"
"os/signal"
"sync/atomic"
"syscall"
"time"
ccconfig "github.com/ClusterCockpit/cc-lib/v2/ccConfig"
"github.com/ClusterCockpit/cc-lib/v2/fleet"
"github.com/ClusterCockpit/cc-lib/v2/nats"
)
// natsClient is set once the application has connected to NATS, which may
// happen only after the fleet configuration delivered the NATS settings.
var natsClient atomic.Pointer[nats.Client]
func applyConfig(json.RawMessage) {}
func useProviders(providers []fleet.Provider) {}
// Example shows the lifecycle of a fleet member: bootstrap, run, apply
// updates from the main loop, close on shutdown.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
cfg, err := fleet.ParseConfig(ccconfig.GetPackageConfig("fleet"))
if err != nil {
log.Fatal(err)
}
client, err := fleet.New(fleet.Options{
Config: cfg,
ServiceType: fleet.ServiceMetricCollector,
Meta: map[string]string{"version": "1.0.0"},
NATS: natsClient.Load, // nil until NATS is connected: REST heartbeats
})
if err != nil {
log.Fatal(err)
}
// Start with the fleet configuration, the cached one, or none at all.
initial, err := client.Bootstrap(ctx, 10*time.Second)
if errors.Is(err, fleet.ErrUnreachable) {
log.Printf("cc-backend unreachable, starting with %s configuration", initial.Source)
} else if err != nil {
log.Printf("fleet bootstrap: %v", err)
}
applyConfig(initial.Config) // nil: no fleet configuration, use local defaults
go func() { _ = client.Run(ctx) }()
defer func() {
closeCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := client.Close(closeCtx); err != nil {
log.Printf("fleet deregistration: %v", err)
}
}()
for {
select {
case <-ctx.Done():
return
case u := <-client.Configs():
applyConfig(u.Config)
case providers := <-client.Rosters():
useProviders(providers) // full replacement, not a delta
}
}
}
Output:
Index ¶
- Constants
- Variables
- func Bucket(cluster string) string
- func DecodeHeartbeats(data []byte) ([]string, error)
- func DiscoverySubject(prefix, bucket string, consumer ServiceType) string
- func EncodeHeartbeat(t time.Time, instanceIDs ...string) ([]byte, error)
- func EncodeRoster(bucket string, consumer ServiceType, providers []Provider, t time.Time) ([]byte, error)
- func ParseHeartbeat(m lp.CCMessage) (string, error)
- type Client
- func (c *Client) Bootstrap(ctx context.Context, timeout time.Duration) (Update, error)
- func (c *Client) Close(ctx context.Context) error
- func (c *Client) Configs() <-chan Update
- func (c *Client) Deregister(ctx context.Context) error
- func (c *Client) Heartbeat(ctx context.Context) error
- func (c *Client) PullConfig(ctx context.Context) (Update, bool, error)
- func (c *Client) Register(ctx context.Context) (RegisterResponse, error)
- func (c *Client) Registered() bool
- func (c *Client) Rosters() <-chan []Provider
- func (c *Client) Run(ctx context.Context) error
- func (c *Client) Scope() Scope
- type Config
- type Options
- type Provider
- type RegisterRequest
- type RegisterResponse
- type Scope
- type ServiceType
- type Source
- type StatusError
- type Update
Examples ¶
Constants ¶
const ( DefaultHeartbeatInterval = 30 * time.Second DefaultConfigPollInterval = 60 * time.Second DefaultTimeout = 10 * time.Second )
Defaults for the optional durations.
const ( // HeartbeatMeasurement is the line-protocol measurement of a heartbeat. HeartbeatMeasurement = "fleet" // HeartbeatFunction is the value of the "function" tag of a heartbeat, // the only function cc-backend accepts on the heartbeat subject. HeartbeatFunction = "heartbeat" // DiscoveryMeasurement is the line-protocol measurement of a roster. DiscoveryMeasurement = "fleetdiscovery" // DefaultDiscoveryPrefix is the default NATS subject prefix of rosters. DefaultDiscoveryPrefix = "cc.fleet.discovery" // InfraBucket is the discovery bucket of infra-scope services. InfraBucket = "infra" // HeaderConfigRevision carries the config revision next to the ETag. HeaderConfigRevision = "X-CC-Config-Revision" )
Wire constants shared by client and server.
const ConfigSchema = `` /* 2001-byte string literal not displayed */
const EnvToken = "CC_FLEET_TOKEN"
EnvToken overrides Config.Token. CC_FLEET_TOKEN_FILE names a file holding the token instead; see util.SecretFromEnv for the precedence rules.
Variables ¶
var ( // ErrUnknownInstance is returned when cc-backend does not know the // instance id (never registered, rotated or deregistered). The client // forgets the id; registering again issues a new one. ErrUnknownInstance = errors.New("fleet: unknown or deregistered instance") // invalid or lacks the api role, or the source IP is not allowlisted. ErrUnauthorized = errors.New("fleet: request not authorized") // ErrNotRegistered is returned by calls that need an instance id before // Register succeeded. ErrNotRegistered = errors.New("fleet: not registered") // ErrUnreachable is returned by Bootstrap when cc-backend could not be // reached at all. ErrUnreachable = errors.New("fleet: cc-backend unreachable") )
var ServiceTypes = []ServiceType{ ServiceMetricStore, ServiceMetricCollector, ServiceBackend, ServiceEventStore, ServiceSlurmAdapter, ServiceNodeController, ServiceEnergyManager, }
ServiceTypes lists all known service types.
Functions ¶
func Bucket ¶
Bucket returns the discovery bucket of a member: its cluster for a cluster-scope service, or InfraBucket when cluster is empty.
func DecodeHeartbeats ¶
DecodeHeartbeats decodes a heartbeat message, which may hold several lines, and returns the instance ids. It fails on the first invalid line.
func DiscoverySubject ¶
func DiscoverySubject(prefix, bucket string, consumer ServiceType) string
DiscoverySubject returns the NATS subject on which a consumer of the given type in the given bucket receives its roster. An empty prefix selects DefaultDiscoveryPrefix.
func EncodeHeartbeat ¶
EncodeHeartbeat encodes one heartbeat line per instance id. Several lines in one message are how an edge aggregator batches heartbeats. cc-backend ignores the timestamp, but the line protocol requires one.
func EncodeRoster ¶
func EncodeRoster(bucket string, consumer ServiceType, providers []Provider, t time.Time) ([]byte, error)
EncodeRoster encodes the roster for one (bucket, consumer type) subject.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a fleet member. Its methods are safe for concurrent use.
func New ¶
New validates the options and creates a client. It does not contact cc-backend. The token is resolved from CC_FLEET_TOKEN, CC_FLEET_TOKEN_FILE or Config.Token, in that order.
func (*Client) Bootstrap ¶
Bootstrap gets the configuration to start with. It loads the cache file (if configured), then registers and pulls within timeout.
On success the returned update has Source SourceFleet, or SourceNone when no configuration is authored for this member. On failure the cached configuration (SourceCache) or an empty update (SourceNone) is returned together with the error: ErrUnreachable for transport errors, a *StatusError otherwise. The application can start in either case; Run keeps trying to register. Call Bootstrap at most once, before Run.
func (*Client) Close ¶
Close stops Run, waits for it to return and deregisters, so the member leaves the discovery rosters at once. ctx bounds the wait and the deregistration. Skipping Close is not fatal: cc-backend marks the member stale after stale-after.
func (*Client) Configs ¶
Configs delivers configuration changes found by Run. The channel holds at most one value; an unread update is replaced by the newer one.
func (*Client) Deregister ¶
Deregister removes the member from the fleet and from discovery rosters at once. It is idempotent: without an instance id, or when cc-backend does not know the id any more, it returns nil.
func (*Client) Heartbeat ¶
Heartbeat marks the member alive. It publishes on the heartbeat subject when one is configured and the NATS client is connected, and falls back to REST otherwise, so heartbeats are never buffered silently by a disconnected client. Only the REST path can detect an unknown instance; with NATS, Run relies on the config poll for that.
func (*Client) PullConfig ¶
PullConfig polls the configuration. changed is true when the delivered configuration differs from the previous one, including the transition to "no configuration" (204, Update.Config nil). An unchanged answer (304, or 200 with an identical body) returns the current configuration with changed false.
func (*Client) Register ¶
func (c *Client) Register(ctx context.Context) (RegisterResponse, error)
Register registers the member and stores the new instance id. Any previous id becomes invalid. The cached ETag is kept, so an unchanged configuration is not delivered again after re-registration.
func (*Client) Registered ¶
Registered reports whether the client holds an instance id.
func (*Client) Rosters ¶
Rosters delivers discovery rosters. Each one is the full provider list, not a delta. The channel holds at most one value; an unread roster is replaced by the newer one.
func (*Client) Run ¶
Run keeps the membership alive until ctx is cancelled or Close is called. It pulls the configuration at once and then every config-poll-interval, sends a heartbeat every heartbeat-interval, (re-)registers whenever needed, and subscribes to the discovery roster once the NATS client is connected. Changes are delivered on Configs and Rosters. Run returns nil on cancellation and an error only if it is already running or closed.
type Config ¶
type Config struct {
URL string `json:"url"` // cc-backend base URL
Token string `json:"token,omitempty"` // JWT with the api role
Cluster string `json:"cluster,omitempty"` // empty selects infra scope
Hostname string `json:"hostname,omitempty"` // default os.Hostname()
HeartbeatInterval string `json:"heartbeat-interval,omitempty"` // default 30s
ConfigPollInterval string `json:"config-poll-interval,omitempty"` // default 60s
HeartbeatSubject string `json:"heartbeat-subject,omitempty"` // empty: REST heartbeats only
DiscoverySubjectPrefix string `json:"discovery-subject-prefix,omitempty"` // default DefaultDiscoveryPrefix
CachePath string `json:"cache-path,omitempty"` // empty: no cache
Timeout string `json:"timeout,omitempty"` // HTTP timeout, default 10s
}
Config is the local fleet section of an application's configuration. It tells the client how to reach cc-backend; it never comes from the fleet service itself.
func ParseConfig ¶
func ParseConfig(raw json.RawMessage) (Config, error)
ParseConfig decodes and validates a fleet configuration section. Unknown keys and malformed durations are errors.
type Options ¶
type Options struct {
Config Config
ServiceType ServiceType
// Meta is registration metadata. It is broadcast unauthenticated in
// discovery rosters: connection hints only, never secrets.
Meta map[string]string
// NATS returns the NATS client used for heartbeats and discovery. It is
// called on every use and may return nil until a connection exists, for
// example because the NATS settings arrive with the fleet configuration.
// Without it the client works over REST only.
NATS func() *nats.Client
// HTTPClient overrides the HTTP client, e.g. for custom TLS. The default
// uses Config.Timeout.
HTTPClient *http.Client
}
Options configures a Client.
type Provider ¶
type Provider struct {
Type ServiceType `json:"type"`
Hostname string `json:"hostname"`
State string `json:"state"`
Meta map[string]string `json:"meta,omitempty"`
}
Provider is one entry of a discovery roster.
func DecodeRoster ¶
DecodeRoster decodes a roster message into the full provider list. An empty roster yields an empty, non-nil slice.
type RegisterRequest ¶
type RegisterRequest struct {
Cluster string `json:"cluster,omitempty"`
Hostname string `json:"hostname"`
ServiceType ServiceType `json:"serviceType"`
MetaData map[string]string `json:"metaData,omitempty"`
}
RegisterRequest is the body of POST /api/fleet/register/{cluster|infra}/.
type RegisterResponse ¶
type RegisterResponse struct {
InstanceID string `json:"instanceId"`
// ConfigRevision is the revision this identity last acknowledged, or 0 if
// it never pulled a configuration.
ConfigRevision int64 `json:"configRevision"`
}
RegisterResponse is the body of a successful (201) registration.
type Scope ¶
type Scope string
Scope selects the registration endpoint and with it the configuration layers and the discovery bucket of a member.
type ServiceType ¶
type ServiceType string
ServiceType is the short code a fleet member declares itself as. It is used as a database value, a directory name in the configuration tree and a NATS subject token.
const ( ServiceMetricStore ServiceType = "ccms" // cc-metric-store ServiceMetricCollector ServiceType = "ccmc" // cc-metric-collector ServiceBackend ServiceType = "ccb" // cc-backend ServiceEventStore ServiceType = "cces" // cc-event-store ServiceSlurmAdapter ServiceType = "ccsa" // cc-slurm-adapter ServiceNodeController ServiceType = "ccnc" // cc-node-controller ServiceEnergyManager ServiceType = "ccem" // cc-energy-manager )
func (ServiceType) Valid ¶
func (t ServiceType) Valid() bool
Valid reports whether t is a known service type.
type Source ¶
type Source int
Source says where a configuration update came from.
const ( // SourceNone means no fleet configuration applies to this member; run // with the local or built-in configuration. SourceNone Source = iota // SourceFleet is a configuration received from cc-backend. SourceFleet // SourceCache is the last known configuration, read from the cache file // because cc-backend was unreachable. SourceCache )
type StatusError ¶
type StatusError struct {
Op string // register, config, heartbeat, deregister
Code int
Status string
Detail string // start of the response body, if any
}
StatusError reports an unexpected HTTP status. It unwraps to ErrUnknownInstance for 404 and to ErrUnauthorized for 401 and 403.
func (*StatusError) Error ¶
func (e *StatusError) Error() string
func (*StatusError) Unwrap ¶
func (e *StatusError) Unwrap() error