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

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:
2026-08-01 15:25:30 +02:00
co-authored by Claude Sonnet 5
parent 282f76a021
commit 2590e404f5
40 changed files with 4105 additions and 181 deletions
+45
View File
@@ -0,0 +1,45 @@
# originclient (Go)
```
go get git.warky.dev/wdevs/origin_client/go
```
## Usage
```go
c, err := originclient.New(originclient.Config{
ServiceKey: "...",
Type: "myservice",
Version: "1.2.3",
Name: "myservice-nova",
})
if err != nil {
log.Fatal(err)
}
go c.Run(ctx) // immediate check-in, then every 24h until ctx is done
```
Call `c.CheckinOnce(ctx)` directly instead of `Run` to send a single
check-in and observe the error/response.
## Config
| Field | Required | Default |
|---|---|---|
| `ServiceKey` | yes | — |
| `Type`, `Version`, `Name` | yes | — |
| `URL` | no | `DefaultURL` (`https://origin.warky.dev/api/public/service-checkin`) |
| `AppType` | no | `"go"` |
| `Hostname` | no | OS hostname |
| `ContainerID` | no | best-effort Docker/cgroup detection |
| `Orchestrator` | no | best-effort Kubernetes/Docker detection |
| `InstallID` | no | OS machine ID, else a generated UUID persisted to `InstallIDPath` |
| `IntervalHours` | no | `DefaultIntervalHours` (24) |
| `Logger` | no | discards warnings |
`Site`, `Environment`, `DatabaseType`, `DatabaseName`, `Port`, `BaseURL`,
`InternalURL`, `Description` are optional metadata with no default.
`Hostname()`, `OutboundIP()`, `ContainerID()`, `Orchestrator()`, and
`ResolveMachineID()` are exported standalone for callers that want the
detection logic without the HTTP client.
+213
View File
@@ -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,
}
}
+148
View File
@@ -0,0 +1,148 @@
package originclient
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testConfig(url string) Config {
return Config{
URL: url,
ServiceKey: "test-key",
Type: "icy2",
Version: "v1.0.0",
Name: "icy2-test",
Site: "Test Site",
Environment: "test",
IntervalHours: 24,
InstallID: "test-install-id",
}
}
func TestNew_RequiresFields(t *testing.T) {
_, err := New(Config{})
assert.Error(t, err)
}
func TestNew_AppliesDefaults(t *testing.T) {
c, err := New(Config{ServiceKey: "k", Type: "t", Version: "v", Name: "n", InstallID: "id"})
require.NoError(t, err)
assert.Equal(t, DefaultURL, c.cfg.URL)
assert.Equal(t, "go", c.cfg.AppType)
assert.Equal(t, DefaultIntervalHours, c.cfg.IntervalHours)
assert.Equal(t, "id", c.installID)
}
func TestBuildPayload(t *testing.T) {
c, err := New(testConfig("https://example.invalid/checkin"))
require.NoError(t, err)
payload := c.buildPayload()
assert.Equal(t, "icy2", payload.Type)
assert.Equal(t, "go", payload.AppType)
assert.Equal(t, "v1.0.0", payload.Version)
assert.Equal(t, "icy2-test", payload.Name)
assert.Equal(t, "Test Site", payload.Site)
assert.Equal(t, "test", payload.Environment)
assert.Equal(t, "test-install-id", payload.UniqueInstallID)
}
func TestCheckinOnce_Success(t *testing.T) {
var gotKey string
var gotBody Payload
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotKey = r.Header.Get("X-Origin-Service-Key")
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody))
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(Response{
Success: true,
ServiceInstanceID: 42,
UniqueInstallID: gotBody.UniqueInstallID,
Status: "provisioning",
PingedAt: "2026-08-01T00:00:00Z",
})
}))
defer server.Close()
c, err := New(testConfig(server.URL))
require.NoError(t, err)
resp, err := c.CheckinOnce(t.Context())
require.NoError(t, err)
assert.Equal(t, "test-key", gotKey)
assert.Equal(t, "icy2", gotBody.Type)
assert.Equal(t, "icy2-test", gotBody.Name)
assert.Equal(t, "test-install-id", gotBody.UniqueInstallID)
assert.True(t, resp.Success)
assert.Equal(t, int64(42), resp.ServiceInstanceID)
}
func TestCheckinOnce_ServerErrorReturnsError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
c, err := New(testConfig(server.URL))
require.NoError(t, err)
resp, err := c.CheckinOnce(t.Context())
assert.Error(t, err)
assert.Nil(t, resp)
}
func TestCheckinOnce_UnreachableDoesNotPanic(t *testing.T) {
c, err := New(testConfig("http://127.0.0.1:1"))
require.NoError(t, err)
assert.NotPanics(t, func() {
_, _ = c.CheckinOnce(t.Context())
})
}
func TestCheckinOnce_LogsOnFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
cfg := testConfig(server.URL)
var warned bool
cfg.Logger = LoggerFunc(func(msg string, err error) { warned = true })
c, err := New(cfg)
require.NoError(t, err)
_, _ = c.CheckinOnce(t.Context())
assert.True(t, warned)
}
func TestRun_ChecksInImmediatelyThenStopsOnContextDone(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(Response{Success: true})
}))
defer server.Close()
c, err := New(testConfig(server.URL))
require.NoError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
defer cancel()
c.Run(ctx)
assert.Equal(t, int32(1), calls.Load())
}
+15
View File
@@ -0,0 +1,15 @@
module git.warky.dev/wdevs/origin_client/go
go 1.24
require (
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
golang.org/x/sys v0.29.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+14
View File
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+100
View File
@@ -0,0 +1,100 @@
package originclient
import (
"net"
"os"
"strings"
)
// Hostname returns the OS hostname, or "" if it cannot be determined.
func Hostname() string {
h, err := os.Hostname()
if err != nil {
return ""
}
return h
}
// OutboundIP returns the first non-loopback IPv4 address bound to this
// host. It is a LAN-facing address, not the host's public internet
// address, and is not part of Origin's wire contract — it exists so
// callers can populate BaseURL/InternalURL/Description themselves.
func OutboundIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok || ipNet.IP.IsLoopback() {
continue
}
if ip4 := ipNet.IP.To4(); ip4 != nil {
return ip4.String()
}
}
return ""
}
// ContainerID best-effort detects the Docker/Kubernetes container ID this
// process runs in. It returns "" outside a container or when detection
// fails; an empty container ID is treated as "not containerized" rather
// than an error.
func ContainerID() string {
if data, err := os.ReadFile("/proc/self/cgroup"); err == nil {
if id := parseCgroupContainerID(data); id != "" {
return id
}
}
if _, err := os.Stat("/.dockerenv"); err == nil {
if h := Hostname(); isContainerIDLike(h) {
return h
}
}
return ""
}
// Orchestrator best-effort detects the orchestration environment:
// "kubernetes" under Kubernetes, "docker" in a plain Docker container, ""
// otherwise.
func Orchestrator() string {
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
return "kubernetes"
}
if _, err := os.Stat("/.dockerenv"); err == nil {
return "docker"
}
return ""
}
// parseCgroupContainerID extracts a container ID from /proc/self/cgroup
// content, handling both cgroup v1 (".../docker/<id>") and cgroup v2
// systemd-scope (".../docker-<id>.scope") layouts.
func parseCgroupContainerID(data []byte) string {
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
idx := strings.LastIndex(line, "/")
if idx == -1 {
continue
}
segment := line[idx+1:]
segment = strings.TrimSuffix(segment, ".scope")
segment = strings.TrimPrefix(segment, "docker-")
if isContainerIDLike(segment) {
return segment
}
}
return ""
}
func isContainerIDLike(s string) bool {
if len(s) < 12 || len(s) > 64 {
return false
}
for _, r := range s {
if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
return false
}
}
return true
}
+57
View File
@@ -0,0 +1,57 @@
package originclient
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseCgroupContainerID(t *testing.T) {
tests := []struct {
name string
data string
want string
}{
{
name: "cgroup v1 docker path",
data: "12:memory:/docker/9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b\n",
want: "9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b",
},
{
name: "cgroup v2 systemd scope",
data: "0::/system.slice/docker-9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b.scope\n",
want: "9f8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b",
},
{
name: "non-container host",
data: "0::/user.slice/user-1000.slice/session-2.scope\n",
want: "",
},
{
name: "empty",
data: "",
want: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, parseCgroupContainerID([]byte(tc.data)))
})
}
}
func TestIsContainerIDLike(t *testing.T) {
assert.True(t, isContainerIDLike("9f8b7c6d5e4f"))
assert.False(t, isContainerIDLike("short"))
assert.False(t, isContainerIDLike("session-2"))
assert.False(t, isContainerIDLike(""))
}
func TestOrchestrator_Kubernetes(t *testing.T) {
t.Setenv("KUBERNETES_SERVICE_HOST", "10.0.0.1")
assert.Equal(t, "kubernetes", Orchestrator())
}
func TestHostname_NotEmpty(t *testing.T) {
assert.NotEmpty(t, Hostname())
}
+20
View File
@@ -0,0 +1,20 @@
package originclient
// Logger receives non-fatal warnings from the client (encode/build/request
// failures, machine-id/persistence fallbacks). It intentionally mirrors the
// smallest common subset of structured loggers so callers can adapt zap,
// slog, logrus, or anything else with one line.
type Logger interface {
Warn(msg string, err error)
}
// LoggerFunc adapts a function to Logger.
type LoggerFunc func(msg string, err error)
// Warn implements Logger.
func (f LoggerFunc) Warn(msg string, err error) { f(msg, err) }
// noopLogger discards every warning.
type noopLogger struct{}
func (noopLogger) Warn(string, error) {}
+87
View File
@@ -0,0 +1,87 @@
package originclient
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
)
// errMachineIDUnavailable is returned by the OS-specific machineID() when
// no real machine ID could be read.
var errMachineIDUnavailable = errors.New("originclient: OS machine id unavailable")
// machineIDFunc is a variable indirection over the OS-specific machineID()
// so tests can stub OS machine-id lookup without depending on the actual
// host's state.
var machineIDFunc = machineID
// ResolveMachineID returns the unique install ID to report to Origin, in
// priority order:
//
// 1. explicit, if non-empty — a caller-supplied override.
// 2. the real OS machine ID (/etc/machine-id on Linux, IOPlatformUUID on
// macOS, MachineGuid on Windows), if readable.
// 3. the ID already persisted at path, if present.
// 4. a newly generated UUID, which ResolveMachineID attempts to persist to
// path for future runs.
//
// OS-ID and persistence failures are logged as warnings, not returned as
// errors: ResolveMachineID always returns a usable ID, falling back to one
// that only lives for this process if it cannot be read from the OS or
// saved to disk. logger may be nil.
func ResolveMachineID(explicit, path string, logger Logger) string {
if logger == nil {
logger = noopLogger{}
}
if id := strings.TrimSpace(explicit); id != "" {
return id
}
if id, err := machineIDFunc(); err == nil {
if id = strings.TrimSpace(id); id != "" {
return id
}
} else {
logger.Warn("machine id: OS machine id unavailable, falling back to a generated id", err)
}
if path == "" {
path = defaultInstallIDPath()
}
if data, err := os.ReadFile(path); err == nil {
if id := strings.TrimSpace(string(data)); id != "" {
return id
}
}
id := uuid.NewString()
if err := persistInstallID(path, id); err != nil {
logger.Warn("machine id: failed to persist generated id, using an ephemeral id for this run", err)
}
return id
}
func persistInstallID(path, id string) error {
if dir := filepath.Dir(path); dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0o750); err != nil {
return err
}
}
return os.WriteFile(path, []byte(id+"\n"), 0o600)
}
// defaultInstallIDPath returns the fallback-UUID persistence path used when
// Config.InstallIDPath is unset: the OS user-config directory, or the OS
// temp directory if that cannot be determined.
func defaultInstallIDPath() string {
dir, err := os.UserConfigDir()
if err != nil || dir == "" {
dir = os.TempDir()
}
return filepath.Join(dir, "origin-client", "install-id")
}
+29
View File
@@ -0,0 +1,29 @@
//go:build darwin
package originclient
import (
"context"
"os/exec"
"regexp"
"time"
)
var ioregUUIDPattern = regexp.MustCompile(`"IOPlatformUUID" = "([0-9A-Fa-f-]+)"`)
// machineID reads the hardware IOPlatformUUID via ioreg, which is stable
// across reboots and unique per physical/virtual Mac.
func machineID() (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "ioreg", "-rd1", "-c", "IOPlatformExpertDevice").Output()
if err != nil {
return "", err
}
match := ioregUUIDPattern.FindSubmatch(out)
if match == nil {
return "", errMachineIDUnavailable
}
return string(match[1]), nil
}
+24
View File
@@ -0,0 +1,24 @@
//go:build linux
package originclient
import (
"os"
"strings"
)
// machineID reads the systemd/dbus machine ID, which is stable across
// reboots and unique per Linux install (but not per container, since it is
// typically inherited from the image unless explicitly reset).
func machineID() (string, error) {
for _, path := range []string{"/etc/machine-id", "/var/lib/dbus/machine-id"} {
data, err := os.ReadFile(path)
if err != nil {
continue
}
if id := strings.TrimSpace(string(data)); id != "" {
return id, nil
}
}
return "", errMachineIDUnavailable
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !linux && !darwin && !windows
package originclient
// machineID has no known OS-specific implementation on this platform;
// ResolveMachineID falls back to a generated, persisted UUID.
func machineID() (string, error) {
return "", errMachineIDUnavailable
}
+61
View File
@@ -0,0 +1,61 @@
package originclient
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func withStubMachineID(t *testing.T, id string, err error) {
t.Helper()
original := machineIDFunc
machineIDFunc = func() (string, error) { return id, err }
t.Cleanup(func() { machineIDFunc = original })
}
func TestResolveMachineID_ExplicitOverrideWins(t *testing.T) {
withStubMachineID(t, "os-machine-id", nil)
got := ResolveMachineID("explicit-id", filepath.Join(t.TempDir(), "install-id"), nil)
assert.Equal(t, "explicit-id", got)
}
func TestResolveMachineID_UsesOSMachineIDWhenAvailable(t *testing.T) {
withStubMachineID(t, "os-machine-id", nil)
got := ResolveMachineID("", filepath.Join(t.TempDir(), "install-id"), nil)
assert.Equal(t, "os-machine-id", got)
}
func TestResolveMachineID_FallsBackAndPersistsWhenOSIDUnavailable(t *testing.T) {
withStubMachineID(t, "", errMachineIDUnavailable)
path := filepath.Join(t.TempDir(), "nested", "install-id")
first := ResolveMachineID("", path, nil)
assert.NotEmpty(t, first)
second := ResolveMachineID("", path, nil)
assert.Equal(t, first, second, "second call should read the persisted id rather than generating a new one")
}
func TestResolveMachineID_NilLoggerDoesNotPanic(t *testing.T) {
withStubMachineID(t, "", errMachineIDUnavailable)
assert.NotPanics(t, func() {
ResolveMachineID("", filepath.Join(t.TempDir(), "install-id"), nil)
})
}
func TestResolveMachineID_LogsWarningWhenOSIDUnavailable(t *testing.T) {
withStubMachineID(t, "", errMachineIDUnavailable)
var warned bool
logger := LoggerFunc(func(msg string, err error) { warned = true })
ResolveMachineID("", filepath.Join(t.TempDir(), "install-id"), logger)
assert.True(t, warned)
}
func TestPersistInstallID(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "dir", "install-id")
require.NoError(t, persistInstallID(path, "abc-123"))
}
+21
View File
@@ -0,0 +1,21 @@
//go:build windows
package originclient
import "golang.org/x/sys/windows/registry"
// machineID reads MachineGuid from the registry, which is generated at
// Windows install time and stable across reboots.
func machineID() (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, registry.QUERY_VALUE|registry.WOW64_64KEY)
if err != nil {
return "", err
}
defer key.Close()
value, _, err := key.GetStringValue("MachineGuid")
if err != nil {
return "", err
}
return value, nil
}
+35
View File
@@ -0,0 +1,35 @@
package originclient
// Payload is the JSON body posted to Origin's public service-checkin
// endpoint. Field names and requiredness mirror
// POST /api/public/service-checkin as documented in
// origin/doc/service-checkin.md: Type, Version, Name, and UniqueInstallID
// are required; everything else is optional and, when omitted, does not
// overwrite existing instance data on Origin's side.
type Payload struct {
Type string `json:"type"`
Version string `json:"version"`
Name string `json:"name"`
UniqueInstallID string `json:"unique_install_id"`
Site string `json:"site,omitempty"`
Environment string `json:"environment,omitempty"`
DatabaseType string `json:"database_type,omitempty"`
DatabaseName string `json:"database_name,omitempty"`
AppType string `json:"app_type,omitempty"`
Hostname string `json:"hostname,omitempty"`
Port int `json:"port,omitempty"`
BaseURL string `json:"base_url,omitempty"`
InternalURL string `json:"internal_url,omitempty"`
ContainerID string `json:"container_id,omitempty"`
Orchestrator string `json:"orchestrator,omitempty"`
Description string `json:"description,omitempty"`
}
// Response is returned by Origin after a successful check-in.
type Response struct {
Success bool `json:"success"`
ServiceInstanceID int64 `json:"service_instance_id"`
UniqueInstallID string `json:"unique_install_id"`
Status string `json:"status"`
PingedAt string `json:"pinged_at"`
}