feat(build): kaniko/trivy 与扫描库改用 registry 内的 mirror 副本,定时刷新并在过期时告警
This commit is contained in:
14 files changed
+426
-82
No files matched your search
+7
-15
@@ -466,22 +466,18 @@ func buildConfig(cfg *config.Config) build.Config {
|
||||
return build.Config{
|
||||
Namespace: cfg.Registry.BuildNamespace,
|
||||
RegistryURL: cfg.Registry.URL,
|
||||
// Empty overrides fall back to the build package's defaults, so an
|
||||
// install that has not imported kaniko/trivy keeps the compiled-in refs
|
||||
// (and fails loudly on pull rather than silently building with the wrong
|
||||
// image).
|
||||
// Empty overrides fall back to the registry's copies of the tools
|
||||
// (build.Tools), which felis mirror-build-tools keeps current.
|
||||
KanikoImage: cfg.Registry.KanikoImage,
|
||||
TrivyImage: cfg.Registry.TrivyImage,
|
||||
CPULimit: cfg.Registry.BuildCPULimit,
|
||||
MemLimit: cfg.Registry.BuildMemLimit,
|
||||
DiskLimit: cfg.Registry.BuildDiskLimit,
|
||||
// "auto" follows the startup probe (see probeBuildUserNamespaces).
|
||||
UserNamespaces: cfg.Registry.BuildUserNamespaces,
|
||||
UserNamespacesProbe: new(atomic.Bool),
|
||||
RuntimeClass: cfg.Registry.BuildRuntimeClass,
|
||||
MaxConcurrent: cfg.Registry.MaxConcurrentBuilds,
|
||||
// Empty keeps Trivy's own default; an install with builds points this at
|
||||
// the internal DB mirror (see config.RegistryConfig.TrivyDBRepository).
|
||||
UserNamespaces: cfg.Registry.BuildUserNamespaces,
|
||||
UserNamespacesProbe: new(atomic.Bool),
|
||||
RuntimeClass: cfg.Registry.BuildRuntimeClass,
|
||||
MaxConcurrent: cfg.Registry.MaxConcurrentBuilds,
|
||||
TrivyDBRepository: cfg.Registry.TrivyDBRepository,
|
||||
TrivyJavaDBRepository: cfg.Registry.TrivyJavaDBRepository,
|
||||
// The submit lane's derived context URLs live here; the fetch step's
|
||||
@@ -669,11 +665,7 @@ func registryPruner(cfg *config.Config, store imageRefStore, servers serverListe
|
||||
fmt.Fprintf(stderr, "felis api: registry pruner disabled (%s unset) — images nothing uses are never deleted from the registry\n", platform.RegistryPruneTokenEnv)
|
||||
return nil
|
||||
}
|
||||
static := []string{
|
||||
os.Getenv("FELIS_IMAGE"),
|
||||
cfg.Registry.KanikoImage, cfg.Registry.TrivyImage,
|
||||
cfg.Registry.TrivyDBRepository, cfg.Registry.TrivyJavaDBRepository,
|
||||
}
|
||||
static := append([]string{os.Getenv("FELIS_IMAGE")}, buildConfig(cfg).ToolRefs()...)
|
||||
return ®istryprune.Pruner{
|
||||
Registry: ®istryprune.Client{Endpoint: "http://" + cfg.Registry.URL, Token: token},
|
||||
Host: cfg.Registry.URL,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"felis.lolicon.best/internal/build"
|
||||
"felis.lolicon.best/internal/imagepush"
|
||||
"felis.lolicon.best/internal/registrygate"
|
||||
)
|
||||
|
||||
// defaultBuildToolsStatus is where mirror-build-tools records its last run; the
|
||||
// watchdog reads it to tell a vulnerability DB that stopped refreshing.
|
||||
const defaultBuildToolsStatus = "/var/lib/felis/build-tools/status.json"
|
||||
|
||||
// cmdMirrorBuildTools copies the build lane's tools (build.Tools: the kaniko and
|
||||
// trivy images, Trivy's vulnerability and Java DBs) from upstream into the
|
||||
// platform registry, where build Jobs pull them. deploy/bootstrap.sh runs it at
|
||||
// install and from felis-build-tools.timer twice a day, which is what keeps the
|
||||
// DBs fresh; a root shell can run it the same way to refresh now.
|
||||
//
|
||||
// It writes as the platform principal through the node's loopback hostPort, the
|
||||
// same way the installer pushes, reading the token from the environment or from
|
||||
// /etc/felis/secrets.env.
|
||||
func cmdMirrorBuildTools(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("mirror-build-tools", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
endpoint := fs.String("endpoint", "127.0.0.1:5000", "host[:port] of the registry to write to (plain HTTP)")
|
||||
only := fs.String("only", "", "comma-separated tool names to copy (default: all of "+toolNames()+")")
|
||||
status := fs.String("status", defaultBuildToolsStatus, `file to record the run in ("" records nothing)`)
|
||||
secrets := fs.String("secrets-env", "/etc/felis/secrets.env", "installer secrets file holding REGISTRY_PLATFORM_TOKEN, read when FELIS_REGISTRY_PASSWORD is unset")
|
||||
platformFlag := fs.String("platform", "", "os/arch of the images to copy (default: this machine's)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
tools, err := selectTools(*only)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "felis mirror-build-tools: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := loadEnvFile(*secrets); err != nil {
|
||||
fmt.Fprintf(stderr, "felis mirror-build-tools: read %s: %v\n", *secrets, err)
|
||||
return 1
|
||||
}
|
||||
user, pass := os.Getenv("FELIS_REGISTRY_USERNAME"), os.Getenv("FELIS_REGISTRY_PASSWORD")
|
||||
if pass == "" {
|
||||
user, pass = registrygate.PrincipalPlatform, os.Getenv("REGISTRY_PLATFORM_TOKEN")
|
||||
}
|
||||
if pass == "" {
|
||||
fmt.Fprintln(stderr, "felis mirror-build-tools: no registry credential: set FELIS_REGISTRY_PASSWORD or run as root on the node (REGISTRY_PLATFORM_TOKEN in /etc/felis/secrets.env)")
|
||||
return 2
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
p := &imagepush.Pusher{Scheme: "http", Username: user, Password: pass, Log: stdout}
|
||||
src := &imagepush.Source{Platform: *platformFlag}
|
||||
started := time.Now()
|
||||
var failed []string
|
||||
for _, t := range tools {
|
||||
dst := strings.TrimSuffix(*endpoint, "/") + "/" + t.Mirror
|
||||
if _, err := p.Mirror(ctx, src, t.Source, dst); err != nil {
|
||||
fmt.Fprintf(stderr, "felis mirror-build-tools: %s: %v\n", t.Name, err)
|
||||
failed = append(failed, t.Name+": "+err.Error())
|
||||
}
|
||||
}
|
||||
if *status != "" {
|
||||
st, err := imagepush.ReadMirrorStatus(*status)
|
||||
if err != nil || st == nil {
|
||||
st = &imagepush.MirrorStatus{}
|
||||
}
|
||||
st.LastAttempt = started
|
||||
st.LastError = strings.Join(failed, "; ")
|
||||
if len(failed) == 0 {
|
||||
st.LastSuccess = started
|
||||
}
|
||||
if err := imagepush.WriteMirrorStatus(*status, *st); err != nil {
|
||||
fmt.Fprintf(stderr, "felis mirror-build-tools: record %s: %v\n", *status, err)
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func toolNames() string {
|
||||
var names []string
|
||||
for _, t := range build.Tools {
|
||||
names = append(names, t.Name)
|
||||
}
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
func selectTools(only string) ([]build.Tool, error) {
|
||||
if only == "" {
|
||||
return build.Tools, nil
|
||||
}
|
||||
var out []build.Tool
|
||||
for _, name := range strings.Split(only, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
found := false
|
||||
for _, t := range build.Tools {
|
||||
if t.Name == name {
|
||||
out = append(out, t)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("unknown tool %q (known: %s)", name, toolNames())
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -90,10 +90,10 @@ type offsiteEnv struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
// loadOffsiteEnvFile sets each KEY=VALUE of path that is not already in the
|
||||
// environment, so a root shell reaches the bucket the same way the unit does.
|
||||
// loadEnvFile sets each KEY=VALUE of path that is not already in the
|
||||
// environment, so a root shell runs a command the same way its unit does.
|
||||
// A missing file is not an error.
|
||||
func loadOffsiteEnvFile(path string) error {
|
||||
func loadEnvFile(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func resolveOffsite(c config.OffsiteConfig) (*offsiteEnv, error) {
|
||||
|
||||
// loadOffsite loads felis.toml and the env file and resolves [offsite].
|
||||
func loadOffsite(cfgPath, envFile string) (*config.Config, *offsiteEnv, error) {
|
||||
if err := loadOffsiteEnvFile(envFile); err != nil {
|
||||
if err := loadEnvFile(envFile); err != nil {
|
||||
return nil, nil, fmt.Errorf("read %s: %w", envFile, err)
|
||||
}
|
||||
cfg, err := config.Load(cfgPath)
|
||||
@@ -454,7 +454,7 @@ func offsiteFetchDB(fs *flag.FlagSet, args []string, stdout, stderr io.Writer) i
|
||||
fmt.Fprint(stderr, offsiteUsage)
|
||||
return 2
|
||||
}
|
||||
if err := loadOffsiteEnvFile(*envFile); err != nil {
|
||||
if err := loadEnvFile(*envFile); err != nil {
|
||||
fmt.Fprintf(stderr, "felis offsite fetch-db: read %s: %v\n", *envFile, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ not a line
|
||||
t.Setenv("FELIS_OFFSITE_ACCESS_KEY", "from-the-shell")
|
||||
t.Setenv("FELIS_OFFSITE_SECRET_KEY", "")
|
||||
t.Setenv("FELIS_OFFSITE_KEY", "")
|
||||
if err := loadOffsiteEnvFile(path); err != nil {
|
||||
if err := loadEnvFile(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, want := range map[string]string{
|
||||
@@ -38,7 +38,7 @@ not a line
|
||||
t.Errorf("%s = %q, want %q", k, got, want)
|
||||
}
|
||||
}
|
||||
if err := loadOffsiteEnvFile(filepath.Join(t.TempDir(), "absent")); err != nil {
|
||||
if err := loadEnvFile(filepath.Join(t.TempDir(), "absent")); err != nil {
|
||||
t.Errorf("a missing env file is not an error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-26
@@ -24,6 +24,7 @@ Commands:
|
||||
egress-gate Hold a build pod until its egress NetworkPolicy is enforced (internal Job entrypoint)
|
||||
fetch-context Fetch and extract a submission's build context (internal Job entrypoint)
|
||||
push-image Push a scanned image tarball to the registry (internal Job entrypoint)
|
||||
mirror-build-tools Copy kaniko, trivy and Trivy's DBs into the registry (run by felis-build-tools.timer)
|
||||
registry-gate Authorize registry writes in front of registry:2 (internal sidecar entrypoint)
|
||||
manifests Render the control-plane RBAC + NetworkPolicy install bundle as YAML
|
||||
apply Create a MinecraftServer CRD (direct K8s write; use -f server.json)
|
||||
@@ -47,32 +48,33 @@ Run "felis <command> -h" for command-specific flags.
|
||||
// The help aliases are deliberately NOT entries: they print usage rather than run a
|
||||
// subcommand, and listing them would make the table disagree with the command list.
|
||||
var commands = map[string]func(args []string, stdout, stderr io.Writer) int{
|
||||
"migrate": cmdMigrate,
|
||||
"db": cmdDB,
|
||||
"offsite": cmdOffsite,
|
||||
"operator": cmdOperator,
|
||||
"api": cmdAPI,
|
||||
"nano": cmdNano,
|
||||
"reaper": cmdReaper,
|
||||
"restore": cmdRestore,
|
||||
"backup": cmdBackup,
|
||||
"files": cmdFiles,
|
||||
"egress-gate": cmdEgressGate,
|
||||
"fetch-context": cmdFetchContext,
|
||||
"push-image": cmdPushImage,
|
||||
"registry-gate": cmdRegistryGate,
|
||||
"manifests": cmdManifests,
|
||||
"apply": cmdApply,
|
||||
"setup": cmdSetup,
|
||||
"converge": cmdConverge,
|
||||
"breakGlass": cmdBreakGlass,
|
||||
"bootstrap-assets": cmdBootstrapAssets,
|
||||
"init-forwarding": cmdInitForwarding,
|
||||
"init-volume": cmdInitVolume,
|
||||
"pin-images": cmdPinImages,
|
||||
"version": cmdVersion,
|
||||
"update": cmdUpdate,
|
||||
"watchdog": cmdWatchdog,
|
||||
"migrate": cmdMigrate,
|
||||
"db": cmdDB,
|
||||
"offsite": cmdOffsite,
|
||||
"operator": cmdOperator,
|
||||
"api": cmdAPI,
|
||||
"nano": cmdNano,
|
||||
"reaper": cmdReaper,
|
||||
"restore": cmdRestore,
|
||||
"backup": cmdBackup,
|
||||
"files": cmdFiles,
|
||||
"egress-gate": cmdEgressGate,
|
||||
"fetch-context": cmdFetchContext,
|
||||
"push-image": cmdPushImage,
|
||||
"mirror-build-tools": cmdMirrorBuildTools,
|
||||
"registry-gate": cmdRegistryGate,
|
||||
"manifests": cmdManifests,
|
||||
"apply": cmdApply,
|
||||
"setup": cmdSetup,
|
||||
"converge": cmdConverge,
|
||||
"breakGlass": cmdBreakGlass,
|
||||
"bootstrap-assets": cmdBootstrapAssets,
|
||||
"init-forwarding": cmdInitForwarding,
|
||||
"init-volume": cmdInitVolume,
|
||||
"pin-images": cmdPinImages,
|
||||
"version": cmdVersion,
|
||||
"update": cmdUpdate,
|
||||
"watchdog": cmdWatchdog,
|
||||
}
|
||||
|
||||
// run dispatches a subcommand. It is separate from main so the router is
|
||||
|
||||
@@ -42,6 +42,7 @@ func cmdWatchdog(args []string, stdout, stderr io.Writer) int {
|
||||
proxyAddr := fs.String("proxy-addr", "", `game proxy address to dial, e.g. 127.0.0.1:25565 ("" skips the check)`)
|
||||
controlNS := fs.String("control-namespace", platform.DefaultControlNamespace, "namespace of the control plane")
|
||||
offsiteStatus := fs.String("offsite-status", offsite.DefaultStatusFile, "the record `felis offsite sync` leaves, checked when [offsite] is configured")
|
||||
toolsStatus := fs.String("build-tools-status", defaultBuildToolsStatus, "the record `felis mirror-build-tools` leaves, checked when builds scan against the registry's DB copy")
|
||||
dryRun := fs.Bool("dry-run", false, "print every finding and the mail that is due; send nothing and keep the state as it was")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -107,6 +108,9 @@ func cmdWatchdog(args []string, stdout, stderr io.Writer) int {
|
||||
if cfg.Offsite.Enabled() {
|
||||
add(watchdog.OffsiteFinding(*offsiteStatus, now))
|
||||
}
|
||||
if usesMirroredScanDB(cfg) {
|
||||
add(watchdog.ScanDBFinding(*toolsStatus, now))
|
||||
}
|
||||
report.Findings = append(report.Findings, watchdog.DiskFindings(splitList(*diskPaths))...)
|
||||
add(watchdog.MemoryFinding("/proc/meminfo"))
|
||||
|
||||
@@ -161,6 +165,17 @@ func cmdWatchdog(args []string, stdout, stderr io.Writer) int {
|
||||
return save()
|
||||
}
|
||||
|
||||
// usesMirroredScanDB reports whether build scans read the vulnerability DB copy
|
||||
// felis mirror-build-tools keeps in the platform registry: the default, or an
|
||||
// explicit trivy_db_repository under the registry's mirror/.
|
||||
func usesMirroredScanDB(cfg *config.Config) bool {
|
||||
if cfg.Registry.URL == "" {
|
||||
return false
|
||||
}
|
||||
repo := cfg.Registry.TrivyDBRepository
|
||||
return repo == "" || strings.HasPrefix(repo, cfg.Registry.URL+"/mirror/")
|
||||
}
|
||||
|
||||
// refreshSMTPPassword caches the relay password from the felis-smtp Secret, or
|
||||
// forgets it when the Secret is gone (a relay without AUTH). An env var named by
|
||||
// [smtp] password_ref, when set, wins at send time instead.
|
||||
|
||||
@@ -279,6 +279,9 @@ WATCHDOG_STATE="/var/lib/felis/watchdog/state.json"
|
||||
OFFSITE_ENV="${STATE_DIR}/offsite.env"
|
||||
OFFSITE_SERVICE="/etc/systemd/system/felis-offsite.service"
|
||||
OFFSITE_TIMER="/etc/systemd/system/felis-offsite.timer"
|
||||
BUILD_TOOLS_SERVICE="/etc/systemd/system/felis-build-tools.service"
|
||||
BUILD_TOOLS_TIMER="/etc/systemd/system/felis-build-tools.timer"
|
||||
BUILD_TOOLS_STATUS="/var/lib/felis/build-tools/status.json"
|
||||
# While this marker holds a future Unix time, felis watchdog mails nothing: an install
|
||||
# restarts the control plane and the system servers on purpose. cleanup removes it; the
|
||||
# time in it is the backstop for an installer killed before its EXIT trap runs.
|
||||
@@ -2679,6 +2682,47 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# The build lane's tools: kaniko and trivy (pinned by digest in internal/build/tools.go)
|
||||
# and Trivy's vulnerability and Java DBs, copied into the registry's mirror/ where build
|
||||
# Jobs pull them; the build namespace has no internet egress. The timer refreshes the DBs
|
||||
# twice a day (upstream publishes every six hours) and the watchdog warns when three days
|
||||
# pass without a clean run. The first copy starts now in the background: the Java DB
|
||||
# alone is several hundred MB.
|
||||
install_build_tools_timer() {
|
||||
install -d -m 0755 "$(dirname "$BUILD_TOOLS_STATUS")"
|
||||
cat > "$BUILD_TOOLS_SERVICE" <<EOF
|
||||
[Unit]
|
||||
Description=Copy the Felis build tools and Trivy's vulnerability DBs into the registry
|
||||
After=network-online.target k3s.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=${HOST_BIN} mirror-build-tools -endpoint ${REGISTRY_PUSH_HOST} -status ${BUILD_TOOLS_STATUS} -secrets-env ${SECRETS_ENV}
|
||||
TimeoutStartSec=1h
|
||||
Nice=10
|
||||
PrivateTmp=yes
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=full
|
||||
EOF
|
||||
cat > "$BUILD_TOOLS_TIMER" <<EOF
|
||||
[Unit]
|
||||
Description=Refresh the Felis build tools and Trivy DBs twice a day
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04,16:00:00
|
||||
RandomizedDelaySec=1h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now felis-build-tools.timer
|
||||
systemctl start --no-block felis-build-tools.service
|
||||
ok "build tools: kaniko, trivy and the Trivy DBs are being copied into the registry (journalctl -u felis-build-tools); refreshed twice a day"
|
||||
}
|
||||
|
||||
# The daily database backup. The first run happens now, so a broken pipeline (pg_dump
|
||||
# missing, directory unwritable) shows up in this install rather than in the first
|
||||
# restore someone needs.
|
||||
@@ -3426,6 +3470,8 @@ main() {
|
||||
# AFTER deploy_bundle: the registry the built images are mirrored into is part
|
||||
# of that bundle.
|
||||
push_images_to_registry
|
||||
# After deploy_bundle, like the pushes: it writes into the registry.
|
||||
install_build_tools_timer
|
||||
restart_existing_system_servers
|
||||
# After deploy_bundle: the proxy dials felis-api's internal ClusterIP, which does not
|
||||
# exist until the bundle is applied.
|
||||
|
||||
+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