feat(build): kaniko/trivy 与扫描库改用 registry 内的 mirror 副本,定时刷新并在过期时告警

This commit is contained in:
Lemon-miaow committed 2026-09-24 23:25:21 +08:00
1 parent c0643af118
commit bf18712a6c
14 files changed
+426 -82

No files matched your search

+15 -10
View File
@@ -234,16 +234,15 @@ type Config struct {
// shape); an install that never builds user submissions can leave it empty.
FelisImage string
// TrivyDBRepository overrides where Trivy fetches its vulnerability DB
// (--db-repository). Empty keeps Trivy's upstream default, which the build
// egress lock denies — an install with builds must point this at an internal
// mirror (see config.RegistryConfig.TrivyDBRepository).
// (--db-repository). Empty means the platform registry's copy (Tools); with no
// RegistryURL it stays empty, which keeps Trivy's upstream default.
TrivyDBRepository string
// TrivyJavaDBRepository overrides where Trivy fetches its Java DB
// (--java-db-repository), downloaded lazily for images that contain Java
// artifacts — i.e. every real modpack. Same egress story as the
// vulnerability DB (see config.RegistryConfig.TrivyJavaDBRepository).
// artifacts — i.e. every real modpack. Defaults like TrivyDBRepository.
TrivyJavaDBRepository string
// KanikoImage / TrivyImage are the executor images.
// KanikoImage / TrivyImage are the executor images. Empty means the platform
// registry's copies (Tools).
KanikoImage string
TrivyImage string
// Deadline caps a build's wall-clock (spec §16: activeDeadlineSeconds).
@@ -301,8 +300,6 @@ func (c Config) userNamespaces() bool {
const (
defaultNamespace = "felis-build"
defaultServiceAccount = "felis-build"
defaultKanikoImage = "gcr.io/kaniko-project/executor:latest"
defaultTrivyImage = "aquasec/trivy:latest"
defaultDeadline = 30 * time.Minute
defaultMaxDockerfile = 256 * 1024 // 256 KiB
defaultCPULimit = "2"
@@ -325,11 +322,19 @@ func (c Config) withDefaults() Config {
if c.ServiceAccount == "" {
c.ServiceAccount = defaultServiceAccount
}
// The executor images and the scan DBs default to the platform registry's
// copies (Tools); an explicit felis.toml value wins.
if c.KanikoImage == "" {
c.KanikoImage = defaultKanikoImage
c.KanikoImage = toolRef(c.RegistryURL, "kaniko")
}
if c.TrivyImage == "" {
c.TrivyImage = defaultTrivyImage
c.TrivyImage = toolRef(c.RegistryURL, "trivy")
}
if c.TrivyDBRepository == "" && c.RegistryURL != "" {
c.TrivyDBRepository = toolRef(c.RegistryURL, "trivy-db")
}
if c.TrivyJavaDBRepository == "" && c.RegistryURL != "" {
c.TrivyJavaDBRepository = toolRef(c.RegistryURL, "trivy-java-db")
}
if c.Deadline <= 0 {
c.Deadline = defaultDeadline
+2 -2
View File
@@ -18,8 +18,8 @@ func sampleJobParams() JobParams {
ServiceAccount: defaultServiceAccount,
RegistryURL: "registry.felis.svc:5000",
FelisImage: "felis:test",
KanikoImage: defaultKanikoImage,
TrivyImage: defaultTrivyImage,
KanikoImage: toolRef("", "kaniko"),
TrivyImage: toolRef("", "trivy"),
Deadline: 30 * time.Minute,
CPULimit: "2",
MemLimit: "4Gi",
+59
View File
@@ -0,0 +1,59 @@
package build
import "strings"
// Tool is an image or OCI artifact a build Job runs or reads: where it comes
// from upstream, and the repository:tag the platform registry keeps its copy
// under. A build pulls only the copy. The build namespace has no internet egress,
// so Trivy cannot reach the upstream DBs, and the node pulls the executor images
// from the in-cluster registry like any other image, which also survives an image
// GC. `felis mirror-build-tools` (deploy/bootstrap.sh runs it at install and from
// felis-build-tools.timer twice a day) copies each Source to its Mirror.
type Tool struct {
Name string
// Source is the upstream reference. The executor images are pinned by the
// digest of their multi-platform index, so a moved or re-pushed upstream tag
// cannot change what a build runs; the DBs follow their tag, since a fresh
// vulnerability DB is the point of refreshing them.
Source string
// Mirror is repository:tag inside the platform registry.
Mirror string
}
// Tools lists every tool a build needs. Kaniko is archived upstream (June 2025)
// and v1.24.0 is its final release; docs/troubleshooting.md §8e covers moving to a
// maintained fork.
var Tools = []Tool{
{Name: "kaniko", Source: "gcr.io/kaniko-project/executor:v1.24.0@sha256:4e7a52dd1f14872430652bb3b027405b8dfd17c4538751c620ac005741ef9698", Mirror: "mirror/kaniko-executor:v1.24.0"},
{Name: "trivy", Source: "ghcr.io/aquasecurity/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969", Mirror: "mirror/trivy:0.74.0"},
{Name: "trivy-db", Source: "mirror.gcr.io/aquasec/trivy-db:2", Mirror: "mirror/trivy-db:2"},
{Name: "trivy-java-db", Source: "mirror.gcr.io/aquasec/trivy-java-db:1", Mirror: "mirror/trivy-java-db:1"},
}
// tool returns the named entry of Tools.
func tool(name string) Tool {
for _, t := range Tools {
if t.Name == name {
return t
}
}
panic("build: unknown tool " + name)
}
// toolRef is where a build reads tool name: its copy in registry, or the upstream
// source when there is no platform registry (tests, a bare Config).
func toolRef(registry, name string) string {
t := tool(name)
if registry == "" {
return t.Source
}
return strings.TrimSuffix(registry, "/") + "/" + t.Mirror
}
// ToolRefs returns the four references a build of this Config reads, after
// defaults: the kaniko and trivy images and the two Trivy DB repositories. The
// registry pruner keeps each of them.
func (c Config) ToolRefs() []string {
c = c.withDefaults()
return []string{c.KanikoImage, c.TrivyImage, c.TrivyDBRepository, c.TrivyJavaDBRepository}
}
+46
View File
@@ -0,0 +1,46 @@
package build
import (
"strings"
"testing"
)
// A build reads every tool from the platform registry's copy by default, and
// each copy's repository lives under mirror/, which only the platform principal
// may write and the pruner keeps.
func TestToolRefsDefaultToTheRegistryCopies(t *testing.T) {
got := Config{RegistryURL: "registry.felis.svc:5000"}.ToolRefs()
want := []string{
"registry.felis.svc:5000/mirror/kaniko-executor:v1.24.0",
"registry.felis.svc:5000/mirror/trivy:0.74.0",
"registry.felis.svc:5000/mirror/trivy-db:2",
"registry.felis.svc:5000/mirror/trivy-java-db:1",
}
if strings.Join(got, " ") != strings.Join(want, " ") {
t.Errorf("ToolRefs = %v, want %v", got, want)
}
explicit := Config{RegistryURL: "r:5000", KanikoImage: "r:5000/mirror/kaniko-fork:v2", TrivyDBRepository: "r:5000/mirror/db:9"}.ToolRefs()
if explicit[0] != "r:5000/mirror/kaniko-fork:v2" || explicit[2] != "r:5000/mirror/db:9" {
t.Errorf("explicit values lost: %v", explicit)
}
// Without a registry the images are the pinned upstream sources and Trivy
// keeps its own DB default.
bare := Config{}.ToolRefs()
if !strings.Contains(bare[0], "@sha256:") || !strings.Contains(bare[1], "@sha256:") || bare[2] != "" || bare[3] != "" {
t.Errorf("bare ToolRefs = %v", bare)
}
}
func TestToolsArePinnedAndMirroredUnderMirror(t *testing.T) {
for _, tl := range Tools {
if !strings.HasPrefix(tl.Mirror, "mirror/") || !strings.Contains(tl.Mirror, ":") {
t.Errorf("%s: mirror %q must be mirror/<repo>:<tag>", tl.Name, tl.Mirror)
}
isDB := strings.HasSuffix(tl.Name, "-db")
if pinned := strings.Contains(tl.Source, "@sha256:"); pinned == isDB {
t.Errorf("%s: source %q: executor images must be pinned by digest, DBs follow their tag", tl.Name, tl.Source)
}
}
}
+12 -22
View File
@@ -148,13 +148,10 @@ type RegistryConfig struct {
URL string `toml:"url"`
BuildNamespace string `toml:"build_namespace"`
// KanikoImage / TrivyImage / BuildCPULimit / BuildMemLimit override the
// build subsystem's compiled-in defaults (gcr.io/kaniko-project/executor and
// aquasec/trivy, 2 CPU / 4Gi per build container). The kubelet pulls the
// executor images over the node's network, so the defaults need a node that
// can reach those registries; an air-gapped or mirrored install points these
// at images mirrored into the in-cluster registry (docs/troubleshooting.md
// §8e). A bare node-containerd import does not survive an image GC, there is
// no pull source for it. Empty keeps the default.
// build subsystem's defaults: the kaniko and trivy copies the installer keeps
// in this registry under mirror/ (build.Tools), 2 CPU / 4Gi per build
// container. Set the images only to run another build of the tools
// (docs/troubleshooting.md §8e). Empty keeps the default.
KanikoImage string `toml:"kaniko_image"`
TrivyImage string `toml:"trivy_image"`
BuildCPULimit string `toml:"build_cpu_limit"`
@@ -173,25 +170,18 @@ type RegistryConfig struct {
// Zero keeps 2; at most 6 (the build namespace's pod quota).
MaxConcurrentBuilds int `toml:"max_concurrent_builds"`
// TrivyDBRepository points Trivy at an OCI repository holding the
// vulnerability DB (--db-repository). Trivy's default fetches from
// mirror.gcr.io/ghcr.io, which the build egress lock denies — so on a
// default install the scan step fails closed and no build ever completes.
// The supported shape is an internal mirror: copy
// mirror.gcr.io/aquasec/trivy-db:2 into this cluster's registry (recipe in
// docs/troubleshooting.md §8) and set this to
// registry.<ns>.svc:5000/mirror/trivy-db:2. The scan runs with --insecure,
// so the plain-HTTP internal registry works. Empty keeps Trivy's own
// default (only usable on an install that deliberately opens internet
// egress to the DB hosts).
// vulnerability DB (--db-repository). Trivy's own default fetches from
// mirror.gcr.io/ghcr.io, which the build egress lock denies, so the default
// here is the copy felis-build-tools.timer refreshes in this registry,
// <url>/mirror/trivy-db:2 (build.Tools). The scan runs with --insecure, so
// the plain-HTTP internal registry works. Empty keeps that default.
TrivyDBRepository string `toml:"trivy_db_repository"`
// TrivyJavaDBRepository points Trivy at an OCI repository holding the Java
// DB (--java-db-repository). Trivy fetches it lazily whenever the scanned
// image contains Java artifacts — every real modpack image does — so on an
// egress-locked box the scan fails closed without this mirror exactly like
// the vulnerability DB. The supported shape is an internal mirror: copy
// mirror.gcr.io/aquasec/trivy-java-db:1 into this cluster's registry and
// set this to registry.<ns>.svc:5000/mirror/trivy-java-db:1 (recipe in
// docs/troubleshooting.md §8e). Empty keeps Trivy's own default.
// egress-locked box it comes from this registry exactly like the
// vulnerability DB: <url>/mirror/trivy-java-db:1 by default. Empty keeps that
// default.
TrivyJavaDBRepository string `toml:"trivy_java_db_repository"`
// UserUploadsContext is the object-store base under which a user-submitted
// modpack's Kaniko build context is pinned. It belongs to the §16 build
+42
View File
@@ -12,6 +12,7 @@ import (
"felis.lolicon.best/internal/apis/felis/v1alpha1"
"felis.lolicon.best/internal/dbbackup"
"felis.lolicon.best/internal/imagepush"
"felis.lolicon.best/internal/naming"
"felis.lolicon.best/internal/offsite"
"felis.lolicon.best/internal/platform"
@@ -45,6 +46,11 @@ const (
maxBackupAge = 26 * time.Hour
// maxReaperAge is the same for the daily reaper CronJob.
maxReaperAge = 26 * time.Hour
// maxScanDBAge is how old the registry's copy of Trivy's vulnerability DB may
// grow. felis-build-tools.timer refreshes it twice a day and upstream publishes
// every six hours; three days of failed refreshes means scans are passing
// images against advisories that are no longer current.
maxScanDBAge = 72 * time.Hour
diskLowRatio = 0.15
diskCriticalRatio = 0.05
@@ -371,6 +377,42 @@ func OffsiteFinding(statusFile string, now time.Time) *Finding {
return f
}
// ScanDBFinding reports the build lane's tool copies (the vulnerability DBs in
// particular) not refreshed within maxScanDBAge, going by the record
// `felis mirror-build-tools` leaves in statusFile. A build still runs and its scan
// still gates, but against an old DB: a warning.
func ScanDBFinding(statusFile string, now time.Time) *Finding {
const hint = "journalctl -u felis-build-tools -n 50; refresh now with `sudo felis mirror-build-tools` (docs/troubleshooting.md §8e)"
st, err := imagepush.ReadMirrorStatus(statusFile)
if err != nil {
return &Finding{
Key: "scan-db", Severity: Warning, For: backupFor,
Summary: fmt.Sprintf("无法读取构建工具镜像状态 %s", statusFile),
SummaryEN: fmt.Sprintf("cannot read the build tools status %s: %v", statusFile, err),
Hint: hint,
}
}
if st != nil && !st.LastSuccess.IsZero() && now.Sub(st.LastSuccess) <= maxScanDBAge {
return nil
}
f := &Finding{
Key: "scan-db", Severity: Warning, For: backupFor,
Summary: "漏洞库从未复制进内置 registry,构建的漏洞扫描无法运行",
SummaryEN: "the vulnerability DB was never copied into the registry; build scans cannot run",
Hint: hint,
}
if st != nil && !st.LastSuccess.IsZero() {
age := roundHours(now.Sub(st.LastSuccess))
f.Summary = fmt.Sprintf("漏洞库已有 %s 没有刷新,构建扫描用的是过期数据", age)
f.SummaryEN = fmt.Sprintf("the vulnerability DB was last refreshed %s ago; build scans use stale advisories", age)
}
if st != nil && st.LastError != "" {
f.Summary += "(最近一次错误:" + st.LastError + ")"
f.SummaryEN += " (last error: " + st.LastError + ")"
}
return f
}
// DiskFindings reports each filesystem under paths that is running out of
// space. Paths on one filesystem are reported once, under the first of them; a
// path that does not exist is skipped (a feature that is not in use).
+22
View File
@@ -12,6 +12,7 @@ import (
"felis.lolicon.best/internal/apis/felis/v1alpha1"
"felis.lolicon.best/internal/dbbackup"
"felis.lolicon.best/internal/imagepush"
"felis.lolicon.best/internal/offsite"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
@@ -210,3 +211,24 @@ func TestDiskFindingsDedupAndSkip(t *testing.T) {
t.Fatalf("findings = %v, want at most one for one filesystem", findingKeys(got))
}
}
func TestScanDBFinding(t *testing.T) {
now := time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)
path := filepath.Join(t.TempDir(), "status.json")
if f := ScanDBFinding(path, now); f == nil || !strings.Contains(f.SummaryEN, "never copied") {
t.Errorf("no status file: %+v", f)
}
if err := imagepush.WriteMirrorStatus(path, imagepush.MirrorStatus{LastAttempt: now, LastSuccess: now.Add(-24 * time.Hour)}); err != nil {
t.Fatal(err)
}
if f := ScanDBFinding(path, now); f != nil {
t.Errorf("a day-old DB: %+v", f)
}
if err := imagepush.WriteMirrorStatus(path, imagepush.MirrorStatus{LastAttempt: now, LastSuccess: now.Add(-100 * time.Hour), LastError: "trivy-db: dial tcp: timeout"}); err != nil {
t.Fatal(err)
}
f := ScanDBFinding(path, now)
if f == nil || f.Severity != Warning || !strings.Contains(f.SummaryEN, "100h") || !strings.Contains(f.SummaryEN, "dial tcp") {
t.Errorf("stale DB: %+v", f)
}
}