Files
package-updater/internal/master/model/model.go
T

217 lines
7.0 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 means the row already exists, or that it changed meanwhile
// (AcceptRunning, MergeService).
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
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
// AwaitingInventory is true from a successful job until the next inventory
// write for the row. Until then DesiredRef is newer than ActualRef without
// the service having drifted.
AwaitingInventory bool
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()
}
// Drifted reports whether the service runs something other than the version
// the last job deployed. A service the updater never touched, a removed one
// and one whose inventory has not caught up with the last job do not drift.
func (s Service) Drifted() bool {
return s.DesiredRef != "" && s.RemovedAt == nil && !s.AwaitingInventory &&
!RunsRef(s.DesiredRef, s.ActualRef, s.ActualDigest)
}
// RunsRef reports whether a service reporting actualRef and actualDigest runs
// the desired ref. The digest decides when both sides know one, so
// "repo:tag@D" is run by "repo:tag" reporting digest D, and by "repo@D".
// Otherwise the repo (compared exactly, without registry normalisation) and
// the followed tag must be equal.
func RunsRef(desired, actualRef, actualDigest string) bool {
d, a := imageref.Parse(desired), imageref.Parse(actualRef)
if d.Repo != a.Repo {
return false
}
ad := a.Digest
if ad == "" {
ad = actualDigest
}
if d.Digest != "" && ad != "" {
return d.Digest == ad
}
return followedTag(d) == followedTag(a)
}
// followedTag is the tag a ref follows: a bare image follows "latest" and a
// ref with a digest but no tag follows nothing.
func followedTag(r imageref.Ref) string {
if r.Tag == "" && r.Digest == "" {
return "latest"
}
return r.Tag
}
// RunningRef is the ref to record as the desired one when the running version
// is accepted: actualRef itself if it carries a digest, "repo:tag@digest" if
// only actualDigest is known (a bare image is "latest"), else actualRef.
func RunningRef(actualRef, actualDigest string) string {
r := imageref.Parse(actualRef)
if r.Digest != "" || !imageref.IsDigest(actualDigest) {
return actualRef
}
r.Tag, r.Digest = followedTag(r), actualDigest
return r.String()
}
// RunningRef returns the ref accepting the running version records as the
// desired one; see the function RunningRef. Templates post it as the guard of
// the accept form.
func (s Service) RunningRef() string { return RunningRef(s.ActualRef, s.ActualDigest) }
// Move says that the removed service FromID has probably moved to the live
// service ToID on another agent. The names are the agents' own; a revoked
// agent keeps its name.
type Move struct {
FromID, ToID int64
FromAgent, ToAgent 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
}