add POST /logs installer log relay to VictoriaLogs
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

A host being PXE-discovered or installed is not in Kubernetes, so vlagent
cannot collect its logs and a failed install leaves no record; the installer
environment has no internal-CA trust or credentials for the HTTPS log ingest,
and bootapi is already the plain-HTTP broker it can reach.

- add POST /logs, token-guarded like POST /provisioned, relaying ndjson to
  vlinsert's jsonline endpoint keyed on serial+phase
- stamp observed source IP and resolved NetBox device name into extra_fields
- return 202 on a sink failure so logs never block an install
- add BOOTAPI_VLINSERT_URL/_TIMEOUT and bootapi_log_relay metrics
This commit is contained in:
2026-10-03 19:44:27 +10:00
parent 0f0fb7fa8e
commit 3c77895788
9 changed files with 416 additions and 5 deletions
+21
View File
@@ -79,6 +79,15 @@ type Config struct {
// endpoint (fail closed). Prefer ProvisionTokenFile in k8s.
ProvisionToken string
// VLInsertURL is the VictoriaLogs vlinsert base that POST /logs relays
// installer logs to. Empty disables the endpoint (it then 503s): a host
// being installed has no vlagent and no credentials for the HTTPS ingest,
// so bootapi relays for it.
VLInsertURL string
// VLInsertTimeout bounds the single best-effort forward attempt. Short on
// purpose — an install must not wait on the log sink.
VLInsertTimeout time.Duration
// PuppetServer / PuppetCAServer are baked into kickstart %post so the
// freshly-installed host checks in to the k8s puppetserver.
PuppetServer string
@@ -118,6 +127,16 @@ func Load() (*Config, error) {
if err != nil {
return nil, fmt.Errorf("invalid BOOTAPI_TEMPLATE_GIT_INTERVAL: %w", err)
}
vlTimeout, err := time.ParseDuration(getenv("BOOTAPI_VLINSERT_TIMEOUT", "5s"))
if err != nil {
return nil, fmt.Errorf("invalid BOOTAPI_VLINSERT_TIMEOUT: %w", err)
}
// Unlike the other URLs, an explicitly EMPTY value is meaningful here: it
// disables the log relay, so LookupEnv rather than getenv.
vlURL, ok := os.LookupEnv("BOOTAPI_VLINSERT_URL")
if !ok {
vlURL = "http://vlinsert-logs.logging.svc.cluster.local:9481"
}
token, err := readSecret("BOOTAPI_NETBOX_TOKEN")
if err != nil {
@@ -169,6 +188,8 @@ func Load() (*Config, error) {
ArtifactBaseURL: strings.TrimRight(getenv("BOOTAPI_ARTIFACT_BASE_URL", "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote"), "/"),
BootBaseURL: strings.TrimRight(os.Getenv("BOOTAPI_BOOT_BASE_URL"), "/"),
ProvisionToken: provToken,
VLInsertURL: strings.TrimRight(vlURL, "/"),
VLInsertTimeout: vlTimeout,
PuppetServer: getenv("BOOTAPI_PUPPET_SERVER", "puppet.k8s.syd1.au.unkin.net"),
PuppetCAServer: getenv("BOOTAPI_PUPPET_CA_SERVER", "puppetca.k8s.syd1.au.unkin.net"),
PuppetCAURL: getenv("BOOTAPI_PUPPET_CA_URL", "puppetca.k8s.syd1.au.unkin.net"),
+28
View File
@@ -40,6 +40,34 @@ func TestLoadDefaults(t *testing.T) {
if c.ArtifactBaseURL != "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote" {
t.Errorf("ArtifactBaseURL = %q", c.ArtifactBaseURL)
}
if c.VLInsertURL != "http://vlinsert-logs.logging.svc.cluster.local:9481" {
t.Errorf("VLInsertURL = %q", c.VLInsertURL)
}
if c.VLInsertTimeout != 5*time.Second {
t.Errorf("VLInsertTimeout = %v, want 5s", c.VLInsertTimeout)
}
}
func TestVLInsertURLEmptyDisablesRelay(t *testing.T) {
clearEnv(t)
// An explicitly empty value must NOT fall back to the default: it disables
// the /logs relay.
t.Setenv("BOOTAPI_VLINSERT_URL", "")
c, err := Load()
if err != nil {
t.Fatal(err)
}
if c.VLInsertURL != "" {
t.Errorf("VLInsertURL = %q, want empty (relay disabled)", c.VLInsertURL)
}
}
func TestVLInsertBadTimeout(t *testing.T) {
clearEnv(t)
t.Setenv("BOOTAPI_VLINSERT_TIMEOUT", "soon")
if _, err := Load(); err == nil {
t.Fatal("expected error for invalid BOOTAPI_VLINSERT_TIMEOUT")
}
}
func TestCallbackBaseDefaultsToBase(t *testing.T) {