The Ledger UI groups Swarm services under their cluster and lists the cluster's managers, so the master needs to know which cluster an agent belongs to. RecordHello now stores hello.SwarmClusterID in the new agents.swarm_cluster_id column. Claude-Session: https://claude.ai/code/session_01GAZsp3P7Qto6JnhipxWUJQ
152 lines
4.6 KiB
Go
152 lines
4.6 KiB
Go
// Package model holds the master's domain types shared by storage, the agent
|
|
// hub and the web UI.
|
|
package model
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shcizo/package-updater/internal/imageref"
|
|
"github.com/shcizo/package-updater/internal/protocol"
|
|
)
|
|
|
|
// Sentinel errors returned by storage.
|
|
var (
|
|
ErrNotFound = errors.New("not found")
|
|
ErrConflict = errors.New("already exists")
|
|
ErrRevoked = errors.New("agent revoked")
|
|
)
|
|
|
|
// Agent is a registered agent. Whether it is connected right now is held in
|
|
// memory by the hub, not here.
|
|
type Agent struct {
|
|
ID int64
|
|
Name string
|
|
Hostname string
|
|
AgentVersion string
|
|
SwarmManager bool
|
|
// SwarmClusterID is the Swarm cluster the agent's daemon was in when it last
|
|
// connected; "" outside a swarm.
|
|
SwarmClusterID string
|
|
LastSeenAt *time.Time
|
|
RevokedAt *time.Time
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// Service is one service as last reported by an agent.
|
|
type Service struct {
|
|
ID int64
|
|
Kind protocol.Kind
|
|
LocationKey string
|
|
AgentID *int64
|
|
AgentName string
|
|
ComposeProject string
|
|
ComposeService string
|
|
WorkingDir string
|
|
ConfigFiles string // raw config_files label, paths joined by ","
|
|
SwarmClusterID string
|
|
SwarmService string
|
|
ContainerName string
|
|
ImageRepo string
|
|
DesiredRef string // set by the last successful job; "" if never updated
|
|
ActualRef string
|
|
ActualDigest string
|
|
Status string
|
|
Self bool
|
|
Built bool // Compose service with a build: section; never updated
|
|
UpdateMode string // ModeManual or ModeAuto
|
|
// AvailableRef is the newest ref a webhook or poll reported for this
|
|
// service; "" if none, or if a later job already deployed it.
|
|
AvailableRef string
|
|
AvailableDigest string // digest of AvailableRef when known (poll); "" from the webhook
|
|
AvailableSource string // SourceWebhook or SourcePoll; "" when AvailableRef is empty
|
|
AvailableAt *time.Time
|
|
LastPollError string // last registry error for the followed ref; "" when fine
|
|
// LastTagDigest is the newest digest of the followed tag that was deployed
|
|
// or started: set when the poller starts an auto job, when it sees the service
|
|
// already running that digest, and when any plain tag job succeeds (the
|
|
// digest the agent pinned); "" if never. The poller does
|
|
// not start another job for the same digest, so a rollback sticks until the
|
|
// registry shows a different one.
|
|
LastTagDigest string
|
|
FirstSeenAt time.Time
|
|
UpdatedAt time.Time
|
|
RemovedAt *time.Time
|
|
}
|
|
|
|
// Update modes of a service.
|
|
const (
|
|
ModeManual = "manual"
|
|
ModeAuto = "auto"
|
|
)
|
|
|
|
// Sources of an available ref and of an ImageVersion.
|
|
const (
|
|
SourceWebhook = "webhook"
|
|
SourcePoll = "poll"
|
|
// SourceRunning is used by image_versions only: an agent reported the
|
|
// version as running.
|
|
SourceRunning = "running"
|
|
)
|
|
|
|
// Available is a newer ref reported for a service.
|
|
type Available struct {
|
|
Ref string
|
|
Digest string // "" when unknown (webhook)
|
|
Source string // SourceWebhook or SourcePoll
|
|
At time.Time
|
|
}
|
|
|
|
// ImageVersion is one version of an image repo the master has seen. Tag and
|
|
// Digest are "" when the version has no tag or its digest is unknown; they
|
|
// are never both empty.
|
|
type ImageVersion struct {
|
|
ID int64
|
|
Repo string
|
|
Tag string
|
|
Digest string
|
|
Source string // SourceWebhook, SourcePoll or SourceRunning: the first source that saw it
|
|
FirstSeenAt time.Time
|
|
}
|
|
|
|
// Ref returns "repo:tag@digest", "repo:tag" or "repo@digest".
|
|
func (v ImageVersion) Ref() string {
|
|
return imageref.Ref{Repo: v.Repo, Tag: v.Tag, Digest: v.Digest}.String()
|
|
}
|
|
|
|
// DisplayName is the human-facing name of the service.
|
|
func (s Service) DisplayName() string {
|
|
switch s.Kind {
|
|
case protocol.KindCompose:
|
|
return s.ComposeProject + "/" + s.ComposeService
|
|
case protocol.KindSwarm:
|
|
return s.SwarmService
|
|
default:
|
|
return s.ContainerName
|
|
}
|
|
}
|
|
|
|
// Session is a logged-in browser session.
|
|
type Session struct {
|
|
ID string
|
|
Subject string
|
|
Name string
|
|
Email string
|
|
CSRFToken string
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// LocationKey turns an agent-relative protocol key into the fleet-wide key
|
|
// stored in services.location_key. Compose services and containers belong to
|
|
// the agent's machine; Swarm services belong to the cluster, so two manager
|
|
// agents in one cluster report the same row.
|
|
func LocationKey(agentID int64, clusterID, agentKey string) string {
|
|
kind, rest, _ := strings.Cut(agentKey, "|")
|
|
if kind == string(protocol.KindSwarm) {
|
|
return kind + "|" + clusterID + "|" + rest
|
|
}
|
|
return kind + "|" + strconv.FormatInt(agentID, 10) + "|" + rest
|
|
}
|