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() }