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.
132 lines
4.1 KiB
Go
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()
|
|
}
|