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.
|
||||
|
||||
Reference in new issue
Block a user