Files
mrhid6 b5b9775d2b
Agent Release / build (push) Successful in 3m40s
Agent Release / msi (push) Successful in 4m54s
fix(patch): gate phases on the window deadline, queue undelivered results, retry startup inventory
The window deadline no longer kills a running package manager: it only gates
the start of each phase, and a started upgrade runs under a 2 hour backstop
that sends SIGTERM on Linux. A PatchResult whose send fails is queued and
flushed on the next command stream, retaking the reboot decision. deb822
folded Suites continuation lines are filtered with the field. The startup
static inventory report is retried until it succeeds.
2026-09-15 13:43:03 +00:00

132 lines
4.1 KiB
Go

package updates
import (
"sort"
"strings"
)
// isSecuritySuite reports whether an apt suite carries security fixes. Debian
// 11+ and every supported Ubuntu name them "<codename>-security".
func isSecuritySuite(s string) bool { return strings.HasSuffix(s, "-security") }
// securitySources reduces a host's apt source files to only the entries that
// point at a security suite, so an upgrade run against them installs security
// fixes and nothing else.
//
// It is a pure function of file contents so it is tested on any platform. The
// caller writes list to a *.list file and deb822 to a *.sources file in a
// temporary SourceParts directory: keeping deb822 paragraphs as deb822 means
// an inline Signed-By key block survives verbatim, which a conversion to
// one-line format could not carry.
//
// ok is false when no security suite exists at all. The caller must then
// report unsupported, never fall back to installing everything.
func securitySources(files map[string]string) (list string, deb822 string, ok bool) {
paths := make([]string, 0, len(files))
for p := range files {
paths = append(paths, p)
}
sort.Strings(paths) // deterministic output
var lb, db strings.Builder
for _, p := range paths {
if strings.HasSuffix(p, ".sources") {
db.WriteString(filterDeb822(files[p]))
} else {
lb.WriteString(filterOneLine(files[p]))
}
}
list, deb822 = lb.String(), db.String()
return list, deb822, list != "" || deb822 != ""
}
func filterOneLine(content string) string {
var b strings.Builder
for _, raw := range strings.Split(content, "\n") {
line := strings.TrimSpace(raw)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
if len(fields) < 3 || fields[0] != "deb" {
continue
}
i := 1
if strings.HasPrefix(fields[i], "[") {
// Options run until the token that closes the bracket.
for i < len(fields) && !strings.HasSuffix(fields[i], "]") {
i++
}
i++
}
// fields[i] is the URI, fields[i+1] the suite.
if i+1 < len(fields) && isSecuritySuite(fields[i+1]) {
b.WriteString(line)
b.WriteString("\n")
}
}
return b.String()
}
// deb822Fields groups a paragraph's lines into fields. A line that starts
// with a space or tab continues the field above it (a folded or multi-line
// value, such as a long Suites list or an inline Signed-By key block).
func deb822Fields(para string) [][]string {
var fields [][]string
for _, l := range strings.Split(strings.Trim(para, "\n"), "\n") {
if (strings.HasPrefix(l, " ") || strings.HasPrefix(l, "\t")) && len(fields) > 0 {
fields[len(fields)-1] = append(fields[len(fields)-1], l)
continue
}
fields = append(fields, []string{l})
}
return fields
}
func filterDeb822(content string) string {
var b strings.Builder
for _, para := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n\n") {
var out []string
isDeb, enabled, kept := false, true, false
for _, field := range deb822Fields(para) {
key, val, found := strings.Cut(field[0], ":")
k := strings.ToLower(strings.TrimSpace(key))
v := strings.TrimSpace(val)
switch {
case found && k == "types":
for _, t := range strings.Fields(v) {
if t == "deb" {
isDeb = true
}
}
case found && k == "enabled":
enabled = strings.ToLower(v) != "no"
case found && k == "suites":
// The value runs across every continuation line. The filtered
// result is written back as one line and the continuation
// lines are dropped with the rest of the original field.
all := strings.Fields(strings.Join(append([]string{v}, field[1:]...), " "))
var sec []string
for _, s := range all {
if isSecuritySuite(s) {
sec = append(sec, s)
}
}
if len(sec) == 0 {
continue // drop the field; the paragraph is dropped below
}
kept = true
out = append(out, "Suites: "+strings.Join(sec, " "))
continue
}
// Every other field, continuation lines included, stays verbatim.
out = append(out, field...)
}
if isDeb && enabled && kept {
b.WriteString(strings.Join(out, "\n"))
b.WriteString("\n\n")
}
}
return b.String()
}