120 lines
3.4 KiB
Go
120 lines
3.4 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shcizo/package-updater/internal/imageref"
|
|
"github.com/shcizo/package-updater/internal/master/jobs"
|
|
"github.com/shcizo/package-updater/internal/master/model"
|
|
"github.com/shcizo/package-updater/internal/master/poller"
|
|
"github.com/shcizo/package-updater/internal/protocol"
|
|
)
|
|
|
|
type serviceGroup struct {
|
|
Title string
|
|
Services []serviceRow
|
|
}
|
|
|
|
// serviceRow is a service as shown in the overview table.
|
|
type serviceRow struct {
|
|
model.Service
|
|
AutoBlocked string // why the service cannot be set to auto; "" when it can
|
|
}
|
|
|
|
// groupServices groups machine-bound services per agent and Swarm services
|
|
// in one group, keeping the store's order (machines first, Swarm last).
|
|
// autoBlocked is called for every service and returns the reason it cannot be
|
|
// set to auto, or "" when it can.
|
|
func groupServices(services []model.Service, autoBlocked func(model.Service) string) []serviceGroup {
|
|
var groups []serviceGroup
|
|
index := map[string]int{}
|
|
for _, s := range services {
|
|
title := s.AgentName
|
|
if s.Kind == protocol.KindSwarm {
|
|
title = "Swarm"
|
|
} else if title == "" {
|
|
title = "Unknown agent"
|
|
}
|
|
i, ok := index[title]
|
|
if !ok {
|
|
i = len(groups)
|
|
index[title] = i
|
|
groups = append(groups, serviceGroup{Title: title})
|
|
}
|
|
groups[i].Services = append(groups[i].Services, serviceRow{Service: s, AutoBlocked: autoBlocked(s)})
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// shortRef shortens a digest in an image reference to 12 hex characters.
|
|
func shortRef(ref string) string {
|
|
r := imageref.Parse(ref)
|
|
if hex, ok := strings.CutPrefix(r.Digest, "sha256:"); ok && len(hex) > 12 {
|
|
r.Digest = "sha256:" + hex[:12] + "…"
|
|
}
|
|
return r.String()
|
|
}
|
|
|
|
// followsNoTag says the service's image reference has a digest but no tag,
|
|
// so there is no tag to poll or update.
|
|
func followsNoTag(svc model.Service) bool {
|
|
return jobs.FollowedRef(svc.ActualRef) == ""
|
|
}
|
|
|
|
// versionRow is an image version as shown on the service page.
|
|
type versionRow struct {
|
|
model.ImageVersion
|
|
Running bool // the service runs exactly this digest
|
|
}
|
|
|
|
// versionRows marks the version the service runs now, by digest.
|
|
func versionRows(versions []model.ImageVersion, actualDigest string) []versionRow {
|
|
rows := make([]versionRow, len(versions))
|
|
for i, v := range versions {
|
|
rows[i] = versionRow{ImageVersion: v, Running: v.Digest != "" && v.Digest == actualDigest}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// pollingStatus is the text of the "Polling" row on the service page and
|
|
// whether it is a registry error.
|
|
func pollingStatus(svc model.Service, interval time.Duration) (text string, failed bool) {
|
|
switch {
|
|
case poller.SkipReason(svc) != "":
|
|
return "not polled: " + poller.SkipReason(svc), false
|
|
case svc.LastPollError != "":
|
|
return "last check failed: " + svc.LastPollError, true
|
|
}
|
|
return "every " + shortDuration(interval), false
|
|
}
|
|
|
|
// shortDuration formats d without trailing zero units: "6h", "1h30m", "90s".
|
|
func shortDuration(d time.Duration) string {
|
|
s := d.String()
|
|
if strings.HasSuffix(s, "m0s") {
|
|
s = s[:len(s)-2]
|
|
}
|
|
if strings.HasSuffix(s, "h0m") {
|
|
s = s[:len(s)-2]
|
|
}
|
|
return s
|
|
}
|
|
|
|
type agentRow struct {
|
|
model.Agent
|
|
Online bool
|
|
}
|
|
|
|
// jobDuration is how long a job ran, or has been running; "" before it starts.
|
|
func jobDuration(j model.Job) string {
|
|
if j.StartedAt == nil {
|
|
return ""
|
|
}
|
|
end := time.Now()
|
|
if j.FinishedAt != nil {
|
|
end = *j.FinishedAt
|
|
}
|
|
return end.Sub(*j.StartedAt).Round(time.Second).String()
|
|
}
|