Generalize distributor report path rendering
This commit is contained in:
@@ -934,7 +934,7 @@ func buildNotificationRequest(cfg config.Config, resolved report.Resolved, repor
|
||||
if err != nil {
|
||||
return NotificationRequest{}, err
|
||||
}
|
||||
bundlePaths, err := config.RenderDistributorReportPaths(cfg.Notify.Distributor.ReportPathTemplates, values)
|
||||
bundlePaths, err := config.RenderDistributorReportPaths("notify.distributor.report_path_templates", cfg.Notify.Distributor.ReportPathTemplates, values)
|
||||
if err != nil {
|
||||
return NotificationRequest{}, err
|
||||
}
|
||||
@@ -964,6 +964,9 @@ func distributorTemplateValuesForReport(cfg config.Config, resolved report.Resol
|
||||
if err := addDistributorValidPeriodValues(&values, resolved.ValidPeriod, cfg.WeatherAPI.Timezone); err != nil {
|
||||
return config.DistributorTemplateValues{}, err
|
||||
}
|
||||
if resolved.Definition.ID == report.Storm {
|
||||
values.StormID = values.ValidStartStamp + "-" + values.ValidEndStamp
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2214,6 +2214,55 @@ func TestResolveGenerateStorm(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorTemplateValuesDeriveStormID(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
now := mustParse("2026-05-29T12:00:00-05:00")
|
||||
start := mustParse("2026-05-29T18:00:00-05:00")
|
||||
end := mustParse("2026-05-30T06:00:00-05:00")
|
||||
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportStorm,
|
||||
StormStart: start,
|
||||
StormEnd: end,
|
||||
}, now)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
}
|
||||
values, err := distributorTemplateValuesForReport(cfg, resolved, "run", "")
|
||||
if err != nil {
|
||||
t.Fatalf("distributorTemplateValuesForReport() error = %v", err)
|
||||
}
|
||||
if values.StormID != "2026-05-29T1800-2026-05-30T0600" {
|
||||
t.Fatalf("StormID = %q, want storm valid-period stamp", values.StormID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorTemplateValuesLeaveStormIDEmptyForOtherReports(t *testing.T) {
|
||||
cfg := config.Defaults()
|
||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||
location, err := timeutil.LoadLocation(cfg.WeatherAPI.Timezone)
|
||||
if err != nil {
|
||||
t.Fatalf("load location: %v", err)
|
||||
}
|
||||
resolved, err := report.Resolve(report.Today, report.ResolveRequest{
|
||||
Now: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
values, err := distributorTemplateValuesForReport(cfg, resolved, "run", "")
|
||||
if err != nil {
|
||||
t.Fatalf("distributorTemplateValuesForReport() error = %v", err)
|
||||
}
|
||||
if values.StormID != "" {
|
||||
t.Fatalf("StormID = %q, want empty for %s", values.StormID, resolved.Definition.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchRunIDUsesUTCStartAndBatchName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -161,7 +161,7 @@ func buildBatchNotificationRequest(cfg config.Config, batch BatchKind, runID str
|
||||
if err != nil {
|
||||
return batchNotificationRequest{}, fmt.Errorf("batch notification report %q run %q source path %q: %w", item.ReportID, item.RunID, item.ReportPath, err)
|
||||
}
|
||||
bundlePaths, err := config.RenderDistributorReportPaths(cfg.Notify.Distributor.ReportPathTemplates, values)
|
||||
bundlePaths, err := config.RenderDistributorReportPaths("notify.distributor.report_path_templates", cfg.Notify.Distributor.ReportPathTemplates, values)
|
||||
if err != nil {
|
||||
return batchNotificationRequest{}, fmt.Errorf("batch notification report %q run %q source path %q: %w", item.ReportID, item.RunID, item.ReportPath, err)
|
||||
}
|
||||
|
||||
@@ -1317,35 +1317,38 @@ func TestDistributorTemplateRendering(t *testing.T) {
|
||||
ValidEndTime: "0600",
|
||||
ValidStartStamp: "2026-06-07T1800",
|
||||
ValidEndStamp: "2026-06-08T0600",
|
||||
StormID: "2026-06-07T1800-2026-06-08T0600",
|
||||
BundleID: "weatherreporter.home.daily",
|
||||
}
|
||||
|
||||
bundleID, err := RenderDistributorBundleID("weatherreporter.{location_id}.{report_id}", values)
|
||||
bundleID, err := RenderDistributorBundleID("weatherreporter.{location_id}.{report_id}.{storm_id}", values)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderDistributorBundleID() error = %v", err)
|
||||
}
|
||||
if bundleID != "weatherreporter.home.daily" {
|
||||
if bundleID != "weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600" {
|
||||
t.Fatalf("bundleID = %q, want rendered value", bundleID)
|
||||
}
|
||||
values.BundleID = bundleID
|
||||
|
||||
pipelineID, err := RenderDistributorPipelineID("weatherreporter.{artifact_group}.{bundle_id}", values)
|
||||
pipelineID, err := RenderDistributorPipelineID("weatherreporter.{artifact_group}.{storm_id}.{bundle_id}", values)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderDistributorPipelineID() error = %v", err)
|
||||
}
|
||||
if pipelineID != "weatherreporter.daily.weatherreporter.home.daily" {
|
||||
if pipelineID != "weatherreporter.daily.2026-06-07T1800-2026-06-08T0600.weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600" {
|
||||
t.Fatalf("pipelineID = %q, want rendered pipeline ID", pipelineID)
|
||||
}
|
||||
|
||||
idempotencyKey, err := RenderDistributorIdempotencyKey("{bundle_id}.{run_id}", values)
|
||||
idempotencyKey, err := RenderDistributorIdempotencyKey("{bundle_id}.{storm_id}.{run_id}", values)
|
||||
if err != nil {
|
||||
t.Fatalf("RenderDistributorIdempotencyKey() error = %v", err)
|
||||
}
|
||||
if idempotencyKey != "weatherreporter.home.daily.20260607T120000Z" {
|
||||
if idempotencyKey != "weatherreporter.home.daily.2026-06-07T1800-2026-06-08T0600.2026-06-07T1800-2026-06-08T0600.20260607T120000Z" {
|
||||
t.Fatalf("idempotencyKey = %q, want rendered run key", idempotencyKey)
|
||||
}
|
||||
|
||||
reportPaths, err := RenderDistributorReportPaths([]string{
|
||||
reportPaths, err := RenderDistributorReportPaths("reports.daily.distributor.path_templates", []string{
|
||||
"{valid_start_date}/{artifact_group}/{valid_start_stamp}-{valid_end_stamp}-{run_id}.md",
|
||||
"storm/{storm_id}/index.md",
|
||||
"{valid_start_date}/{artifact_group}/latest.md",
|
||||
}, values)
|
||||
if err != nil {
|
||||
@@ -1353,6 +1356,7 @@ func TestDistributorTemplateRendering(t *testing.T) {
|
||||
}
|
||||
wantPaths := []string{
|
||||
"2026-06-07/daily/2026-06-07T1800-2026-06-08T0600-20260607T120000Z.md",
|
||||
"storm/2026-06-07T1800-2026-06-08T0600/index.md",
|
||||
"2026-06-07/daily/latest.md",
|
||||
}
|
||||
if strings.Join(reportPaths, "\n") != strings.Join(wantPaths, "\n") {
|
||||
@@ -1360,6 +1364,50 @@ func TestDistributorTemplateRendering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorReportPathRenderingUsesCallerName(t *testing.T) {
|
||||
values := DistributorTemplateValues{
|
||||
BatchOutputName: "report.md",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
templates []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "UnknownVariable",
|
||||
templates: []string{"{unknown}.md"},
|
||||
wantErr: `report.daily.distributor_path_templates[0] contains unknown template variable "unknown"`,
|
||||
},
|
||||
{
|
||||
name: "InvalidPath",
|
||||
templates: []string{"/{batch_output_name}"},
|
||||
wantErr: "report.daily.distributor_path_templates[0] must render a relative path",
|
||||
},
|
||||
{
|
||||
name: "DuplicatePath",
|
||||
templates: []string{"latest.md", "latest.md"},
|
||||
wantErr: `report.daily.distributor_path_templates renders duplicate path "latest.md"`,
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
templates: nil,
|
||||
wantErr: "report.daily.distributor_path_templates must contain at least one entry",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := RenderDistributorReportPaths("report.daily.distributor_path_templates", tt.templates, values)
|
||||
if err == nil {
|
||||
t.Fatal("RenderDistributorReportPaths() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("error = %q, want %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistributorBatchTemplateRendering(t *testing.T) {
|
||||
values := DistributorBatchTemplateValues{
|
||||
LocationID: "home",
|
||||
@@ -1492,12 +1540,15 @@ func TestDistributorReportPathRenderingRejectsInvalidValues(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := RenderDistributorReportPaths([]string{"{batch_output_name}"}, DistributorTemplateValues{
|
||||
_, err := RenderDistributorReportPaths("report.daily.distributor_path_templates", []string{"{batch_output_name}"}, DistributorTemplateValues{
|
||||
BatchOutputName: tt.batchOutputName,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RenderDistributorReportPaths() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "report.daily.distributor_path_templates[0]") {
|
||||
t.Fatalf("error = %q, want caller path name", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ type DistributorTemplateValues struct {
|
||||
ValidEndTime string
|
||||
ValidStartStamp string
|
||||
ValidEndStamp string
|
||||
StormID string
|
||||
BundleID string
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ var distributorTemplateVariables = map[string]struct{}{
|
||||
"valid_end_time": {},
|
||||
"valid_start_stamp": {},
|
||||
"valid_end_stamp": {},
|
||||
"storm_id": {},
|
||||
}
|
||||
|
||||
var distributorIdempotencyTemplateVariables = map[string]struct{}{
|
||||
@@ -55,6 +57,7 @@ var distributorIdempotencyTemplateVariables = map[string]struct{}{
|
||||
"valid_end_time": {},
|
||||
"valid_start_stamp": {},
|
||||
"valid_end_stamp": {},
|
||||
"storm_id": {},
|
||||
"bundle_id": {},
|
||||
}
|
||||
|
||||
@@ -129,23 +132,23 @@ func RenderDistributorBatchIdempotencyKey(template string, values DistributorBat
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func RenderDistributorReportPaths(templates []string, values DistributorTemplateValues) ([]string, error) {
|
||||
func RenderDistributorReportPaths(name string, templates []string, values DistributorTemplateValues) ([]string, error) {
|
||||
if len(templates) == 0 {
|
||||
return nil, fmt.Errorf("notify.distributor.report_path_templates must contain at least one entry")
|
||||
return nil, fmt.Errorf("%s must contain at least one entry", name)
|
||||
}
|
||||
paths := make([]string, 0, len(templates))
|
||||
seen := make(map[string]struct{}, len(templates))
|
||||
for i, template := range templates {
|
||||
name := fmt.Sprintf("notify.distributor.report_path_templates[%d]", i)
|
||||
rendered, err := renderDistributorTemplate(name, template, values, distributorTemplateVariables)
|
||||
itemName := fmt.Sprintf("%s[%d]", name, i)
|
||||
rendered, err := renderDistributorTemplate(itemName, template, values, distributorTemplateVariables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateDistributorReportPath(name, rendered); err != nil {
|
||||
if err := ValidateDistributorReportPath(itemName, rendered); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := seen[rendered]; ok {
|
||||
return nil, fmt.Errorf("notify.distributor.report_path_templates renders duplicate path %q", rendered)
|
||||
return nil, fmt.Errorf("%s renders duplicate path %q", name, rendered)
|
||||
}
|
||||
seen[rendered] = struct{}{}
|
||||
paths = append(paths, rendered)
|
||||
@@ -243,6 +246,8 @@ func distributorTemplateValue(variable string, values DistributorTemplateValues)
|
||||
return values.ValidStartStamp
|
||||
case "valid_end_stamp":
|
||||
return values.ValidEndStamp
|
||||
case "storm_id":
|
||||
return values.StormID
|
||||
case "bundle_id":
|
||||
return values.BundleID
|
||||
default:
|
||||
|
||||
@@ -123,26 +123,8 @@ func traverseReportDistributorPathOverrides(cfg Config) (map[report.ID][]string,
|
||||
|
||||
func validateReportDistributorPathTemplates(reportKey string, templates []string) error {
|
||||
name := fmt.Sprintf("reports.%s.distributor.path_templates", reportKey)
|
||||
if len(templates) == 0 {
|
||||
return fmt.Errorf("%s must contain at least one entry", name)
|
||||
}
|
||||
values := sampleDistributorTemplateValues()
|
||||
seen := map[string]struct{}{}
|
||||
for i, template := range templates {
|
||||
itemName := fmt.Sprintf("%s[%d]", name, i)
|
||||
rendered, err := renderDistributorTemplate(itemName, template, values, distributorTemplateVariables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateDistributorReportPath(itemName, rendered); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := seen[rendered]; ok {
|
||||
return fmt.Errorf("%s renders duplicate path %q", name, rendered)
|
||||
}
|
||||
seen[rendered] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
_, err := RenderDistributorReportPaths(name, templates, sampleDistributorTemplateValues())
|
||||
return err
|
||||
}
|
||||
|
||||
func sampleDistributorTemplateValues() DistributorTemplateValues {
|
||||
@@ -158,6 +140,7 @@ func sampleDistributorTemplateValues() DistributorTemplateValues {
|
||||
ValidEndTime: "0000",
|
||||
ValidStartStamp: "2026-05-29T0000",
|
||||
ValidEndStamp: "2026-05-30T0000",
|
||||
StormID: "2026-05-29T0000-2026-05-30T0000",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ func validateDistributorNotify(cfg DistributorNotifyConfig) error {
|
||||
if _, err := RenderDistributorPipelineID(cfg.PipelineIDTemplate, values); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := RenderDistributorReportPaths(cfg.ReportPathTemplates, values); err != nil {
|
||||
if _, err := RenderDistributorReportPaths("notify.distributor.report_path_templates", cfg.ReportPathTemplates, values); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDistributorBatchNotify(cfg.Batch); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user