package main // The container runtime's log rotation, checked before it reaches daemon.json (novox/hq issue 452). // // **Why a step and not the file alone.** daemon.json's log-driver and log-opts are filled by the controller // from this module's settings log-max-size and log-max-file, pasted as text (novox/hq issue 346): the // controller judges nothing but that a value is one line. The runtime is the only reader of daemon.json and // reads log-opts only when it starts, so a bad value would be found at the runtime's next start, as a runtime // that does not start, and a value with a quote in it could add any key to the file. So the module's // manifest writes the same two values into log-rotation.conf first, and runs this as a step before daemon.json: // a value refused here fails the step, and the host skips the rest of the module (novox/hq ADR 0136), so // daemon.json keeps what it had, and the refusal is in the apply's report. import ( "fmt" "os" "regexp" "strings" ) // logRotation is what log-rotation.conf says, both values as the runtime takes them: text. type logRotation struct{ Size, Files string } var ( // A size as the json-file driver reads it, with the unit required and in lower case, as decided on issue // 452: a whole number from 1 with k, m or g, at most nine digits, so no value overflows the runtime's count. logSize = regexp.MustCompile(`^[1-9][0-9]{0,8}[kmg]$`) // A count of files from 1 to 99, without a leading zero. logFiles = regexp.MustCompile(`^[1-9][0-9]?$`) ) // parseLogRotation reads `size=` and `files=` once each; a comment or an empty line is skipped, and any other // line is refused, so a value that brought a line of its own is never read past. func parseLogRotation(text string) (logRotation, error) { var got logRotation seen := map[string]bool{} for _, line := range strings.Split(text, "\n") { if line == "" || strings.HasPrefix(line, "#") { continue } key, value, ok := strings.Cut(line, "=") if !ok || (key != "size" && key != "files") { return logRotation{}, fmt.Errorf("log-rotation.conf has a line that is not size= or files=: %q", line) } if seen[key] { return logRotation{}, fmt.Errorf("log-rotation.conf says %s twice", key) } seen[key] = true switch key { case "size": if !logSize.MatchString(value) { return logRotation{}, fmt.Errorf("log-max-size %q is not a size: a whole number from 1 with k, m or g "+ "after it, such as 100m; daemon.json was left as it is", value) } got.Size = value case "files": if !logFiles.MatchString(value) { return logRotation{}, fmt.Errorf("log-max-file %q is not a count from 1 to 99; daemon.json was left "+ "as it is", value) } got.Files = value } } for _, key := range []string{"size", "files"} { if !seen[key] { return logRotation{}, fmt.Errorf("log-rotation.conf does not say %s", key) } } return got, nil } // checkLogRotationFile is the step: the file the manifest wrote, judged. func checkLogRotationFile(path string) error { raw, err := os.ReadFile(path) if err != nil { return err } _, err = parseLogRotation(string(raw)) return err }