Private
Public Access
feat: initial Go/JS/Rust Origin check-in client SDKs
CI / rust (push) Successful in 3m6s
CI / go (push) Successful in 33s
CI / js (push) Successful in 34s
CI / rust (push) Successful in 3m6s
CI / go (push) Successful in 33s
CI / js (push) Successful in 34s
Standalone clients that resolve a machine unique ID (OS machine ID first, falling back to a generated UUID persisted to disk), detect hostname/IP/ container/orchestrator, and register+re-ping origin.warky.dev's public service-checkin endpoint on a daily interval. Ported from icy2's internal origincheckin package but extended to the full check-in contract and made dependency-light for external reuse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+213
@@ -0,0 +1,213 @@
|
||||
// Package originclient registers a running service with Origin
|
||||
// (origin.warky.dev)'s public check-in endpoint and re-pings it on an
|
||||
// interval, so Origin can track which instances of a service are alive,
|
||||
// their version, and where they run. A check-in failure is logged and
|
||||
// otherwise ignored: it must never affect the host application's own
|
||||
// availability.
|
||||
package originclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultURL is Origin's public service-checkin endpoint.
|
||||
DefaultURL = "https://origin.warky.dev/api/public/service-checkin"
|
||||
// DefaultIntervalHours is how often Run repeats when Config.IntervalHours is unset.
|
||||
DefaultIntervalHours = 24
|
||||
|
||||
requestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Config configures a Client. ServiceKey, Type, Version, and Name identify
|
||||
// the caller to Origin and are required; everything else is optional
|
||||
// metadata, or overrides for values Client would otherwise auto-detect.
|
||||
type Config struct {
|
||||
// URL is the check-in endpoint. Defaults to DefaultURL.
|
||||
URL string
|
||||
// ServiceKey is sent as the X-Origin-Service-Key header. Required.
|
||||
ServiceKey string
|
||||
|
||||
// Type is the Origin service type, which must already be registered in
|
||||
// Origin (e.g. "icy2"). Required.
|
||||
Type string
|
||||
// Version is the caller's own version string. Required.
|
||||
Version string
|
||||
// Name identifies this deployment/instance to Origin, e.g. "icy2-nova". Required.
|
||||
Name string
|
||||
|
||||
Site string
|
||||
Environment string
|
||||
DatabaseType string
|
||||
DatabaseName string
|
||||
// AppType defaults to "go" when empty.
|
||||
AppType string
|
||||
// Hostname defaults to the OS hostname (see Hostname) when empty.
|
||||
Hostname string
|
||||
Port int
|
||||
BaseURL string
|
||||
InternalURL string
|
||||
// ContainerID defaults to best-effort Docker/cgroup detection (see
|
||||
// ContainerID) when empty.
|
||||
ContainerID string
|
||||
// Orchestrator defaults to best-effort Kubernetes/Docker detection (see
|
||||
// Orchestrator) when empty.
|
||||
Orchestrator string
|
||||
Description string
|
||||
|
||||
// InstallID, if set, overrides machine-ID resolution entirely.
|
||||
InstallID string
|
||||
// InstallIDPath is where a generated fallback install ID is persisted
|
||||
// when no real OS machine ID is available. Defaults to the OS
|
||||
// user-config directory (see ResolveMachineID).
|
||||
InstallIDPath string
|
||||
|
||||
// IntervalHours sets how often Run repeats. Defaults to DefaultIntervalHours.
|
||||
IntervalHours int
|
||||
|
||||
// Logger receives non-fatal warnings. Defaults to discarding them.
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// Client checks a service in with Origin.
|
||||
type Client struct {
|
||||
cfg Config
|
||||
http *http.Client
|
||||
installID string
|
||||
}
|
||||
|
||||
// New validates cfg, resolves the machine/install ID, and returns a ready
|
||||
// Client. Resolution happens once, at construction, so every check-in made
|
||||
// by this Client reports the same UniqueInstallID.
|
||||
func New(cfg Config) (*Client, error) {
|
||||
if cfg.ServiceKey == "" || cfg.Type == "" || cfg.Version == "" || cfg.Name == "" {
|
||||
return nil, errors.New("originclient: ServiceKey, Type, Version, and Name are required")
|
||||
}
|
||||
if cfg.URL == "" {
|
||||
cfg.URL = DefaultURL
|
||||
}
|
||||
if cfg.AppType == "" {
|
||||
cfg.AppType = "go"
|
||||
}
|
||||
if cfg.IntervalHours <= 0 {
|
||||
cfg.IntervalHours = DefaultIntervalHours
|
||||
}
|
||||
if cfg.Logger == nil {
|
||||
cfg.Logger = noopLogger{}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
http: &http.Client{Timeout: requestTimeout},
|
||||
installID: ResolveMachineID(cfg.InstallID, cfg.InstallIDPath, cfg.Logger),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run performs an immediate check-in, then repeats every
|
||||
// Config.IntervalHours until ctx is done. Run blocks the calling goroutine,
|
||||
// so callers invoke it with `go client.Run(ctx)`. Check-in errors are
|
||||
// logged via Config.Logger and otherwise ignored; call CheckinOnce directly
|
||||
// if the caller needs to observe failures.
|
||||
func (c *Client) Run(ctx context.Context) {
|
||||
_, _ = c.CheckinOnce(ctx)
|
||||
|
||||
ticker := time.NewTicker(time.Duration(c.cfg.IntervalHours) * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
_, _ = c.CheckinOnce(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckinOnce sends a single check-in request and returns Origin's
|
||||
// response. Failures are logged via Config.Logger and also returned, so
|
||||
// callers that only want the fire-and-forget behavior of Run can ignore
|
||||
// the returned error.
|
||||
func (c *Client) CheckinOnce(ctx context.Context) (*Response, error) {
|
||||
payload := c.buildPayload()
|
||||
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
c.cfg.Logger.Warn("origin checkin: encode payload failed", err)
|
||||
return nil, fmt.Errorf("originclient: encode payload: %w", err)
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, requestTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.cfg.URL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
c.cfg.Logger.Warn("origin checkin: build request failed", err)
|
||||
return nil, fmt.Errorf("originclient: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Origin-Service-Key", c.cfg.ServiceKey)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
c.cfg.Logger.Warn("origin checkin: request failed", err)
|
||||
return nil, fmt.Errorf("originclient: do request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
err := fmt.Errorf("originclient: non-2xx response: %d", resp.StatusCode)
|
||||
c.cfg.Logger.Warn("origin checkin: non-2xx response", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Response
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
c.cfg.Logger.Warn("origin checkin: decode response failed", err)
|
||||
return nil, fmt.Errorf("originclient: decode response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildPayload() Payload {
|
||||
hostname := c.cfg.Hostname
|
||||
if hostname == "" {
|
||||
hostname = Hostname()
|
||||
}
|
||||
containerID := c.cfg.ContainerID
|
||||
if containerID == "" {
|
||||
containerID = ContainerID()
|
||||
}
|
||||
orchestrator := c.cfg.Orchestrator
|
||||
if orchestrator == "" {
|
||||
orchestrator = Orchestrator()
|
||||
}
|
||||
|
||||
return Payload{
|
||||
Type: c.cfg.Type,
|
||||
Version: c.cfg.Version,
|
||||
Name: c.cfg.Name,
|
||||
UniqueInstallID: c.installID,
|
||||
Site: c.cfg.Site,
|
||||
Environment: c.cfg.Environment,
|
||||
DatabaseType: c.cfg.DatabaseType,
|
||||
DatabaseName: c.cfg.DatabaseName,
|
||||
AppType: c.cfg.AppType,
|
||||
Hostname: hostname,
|
||||
Port: c.cfg.Port,
|
||||
BaseURL: c.cfg.BaseURL,
|
||||
InternalURL: c.cfg.InternalURL,
|
||||
ContainerID: containerID,
|
||||
Orchestrator: orchestrator,
|
||||
Description: c.cfg.Description,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user