From 1d3ea64541ce9a794b1c9d0db30a469ded028963 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 13 Aug 2026 00:21:20 +0000 Subject: [PATCH] Require nonblank notification identities --- docs/config.md | 3 +++ docs/integrations/distributor/api.md | 4 ++-- docs/operations.md | 2 ++ internal/config/config_test.go | 14 ++++++++++++++ internal/config/notify_templates.go | 18 ++++++++++++++++-- internal/config/validate.go | 3 +++ 6 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/config.md b/docs/config.md index e923da6..12b889a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -135,6 +135,9 @@ Distributor notification is disabled by default. Its fields are: The upload token is read from the environment variable named by `token_env`. Use `secrets.directory` when a file-backed secret is appropriate. +When notification is enabled, each rendered single-report pipeline ID, bundle +ID, and idempotency key must contain at least one non-whitespace character. + Single-report bundle templates accept `location_id`, `report_id`, `run_id`, `artifact_group`, `batch_output_name`, `valid_start_date`, `valid_end_date`, `valid_start_time`, `valid_end_time`, `valid_start_stamp`, `valid_end_stamp`, diff --git a/docs/integrations/distributor/api.md b/docs/integrations/distributor/api.md index 2d8e049..9dfcd2a 100644 --- a/docs/integrations/distributor/api.md +++ b/docs/integrations/distributor/api.md @@ -23,8 +23,8 @@ A successful response is `202 Accepted` with JSON containing `run_id` and `status`. Acceptance means Distributor staged and validated the source bundle; it does not mean downstream destinations have published it. -The adapter requires a pipeline ID, bundle ID, idempotency key, and at least one -source-file mapping before calling Distributor. It reads the bearer token from +The adapter requires nonblank pipeline ID, bundle ID, and idempotency key, plus +at least one source-file mapping, before calling Distributor. It reads the bearer token from the configured environment variable and redacts that value from errors. Request construction and timeout handling belong to the [Distributor adapter](../../internal/distributor-adapter.md). diff --git a/docs/operations.md b/docs/operations.md index 582c9c8..bbdbfed 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -69,6 +69,8 @@ returns a failed batch status even when all report counters show success; the top-level notification result contains the delivery diagnostic. For a single report, Distributor notification follows the atomic output write. +Enabled notification templates are validated before report processing, including +the requirement that each rendered identity contains a non-whitespace character. See the [configuration reference](config.md) for pipeline, bundle, idempotency-key, and per-report path templates. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b3de296..a1397c5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1285,6 +1285,13 @@ func TestEnabledDistributorNotifyValidation(t *testing.T) { }, wantErr: "notify.distributor.bundle_id_template", }, + { + name: "BundleTemplateRenderedEmpty", + mutate: func(cfg *Config) { + cfg.Notify.Distributor.BundleIDTemplate = " " + }, + wantErr: "notify.distributor.bundle_id_template", + }, { name: "IdempotencyTemplate", mutate: func(cfg *Config) { @@ -1292,6 +1299,13 @@ func TestEnabledDistributorNotifyValidation(t *testing.T) { }, wantErr: "notify.distributor.idempotency_key_template", }, + { + name: "IdempotencyTemplateRenderedEmpty", + mutate: func(cfg *Config) { + cfg.Notify.Distributor.IdempotencyKeyTemplate = " " + }, + wantErr: "notify.distributor.idempotency_key_template", + }, { name: "BatchTemplate", mutate: func(cfg *Config) { diff --git a/internal/config/notify_templates.go b/internal/config/notify_templates.go index 87052af..0d627fb 100644 --- a/internal/config/notify_templates.go +++ b/internal/config/notify_templates.go @@ -78,7 +78,14 @@ var distributorBatchIdempotencyTemplateVariables = map[string]struct{}{ var distributorBatchPipelineTemplateVariables = distributorBatchTemplateVariables func RenderDistributorBundleID(template string, values DistributorTemplateValues) (string, error) { - return renderDistributorTemplate("notify.distributor.bundle_id_template", template, values, distributorTemplateVariables) + rendered, err := renderDistributorTemplate("notify.distributor.bundle_id_template", template, values, distributorTemplateVariables) + if err != nil { + return "", err + } + if strings.TrimSpace(rendered) == "" { + return "", fmt.Errorf("notify.distributor.bundle_id_template renders an empty bundle id") + } + return rendered, nil } func RenderDistributorPipelineID(template string, values DistributorTemplateValues) (string, error) { @@ -93,7 +100,14 @@ func RenderDistributorPipelineID(template string, values DistributorTemplateValu } func RenderDistributorIdempotencyKey(template string, values DistributorTemplateValues) (string, error) { - return renderDistributorTemplate("notify.distributor.idempotency_key_template", template, values, distributorIdempotencyTemplateVariables) + rendered, err := renderDistributorTemplate("notify.distributor.idempotency_key_template", template, values, distributorIdempotencyTemplateVariables) + if err != nil { + return "", err + } + if strings.TrimSpace(rendered) == "" { + return "", fmt.Errorf("notify.distributor.idempotency_key_template renders an empty idempotency key") + } + return rendered, nil } func RenderDistributorBatchBundleID(template string, values DistributorBatchTemplateValues) (string, error) { diff --git a/internal/config/validate.go b/internal/config/validate.go index 244f635..c1ef356 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -152,6 +152,9 @@ func validateDistributorNotify(cfg DistributorNotifyConfig) error { if _, err := RenderDistributorPipelineID(cfg.PipelineIDTemplate, values); err != nil { return err } + if _, err := RenderDistributorIdempotencyKey(cfg.IdempotencyKeyTemplate, values); err != nil { + return err + } if err := validateDistributorBatchNotify(cfg.Batch); err != nil { return err }