feat(build): kaniko/trivy 与扫描库改用 registry 内的 mirror 副本,定时刷新并在过期时告警
This commit is contained in:
14 files changed
+426
-82
No files matched your search
+15
-10
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user