package config import ( "fmt" "path/filepath" "strings" ) type DistributorTemplateValues struct { LocationID string ReportID string RunID string ArtifactGroup string BatchOutputName string ValidStartDate string ValidEndDate string ValidStartTime string ValidEndTime string ValidStartStamp string ValidEndStamp string StormID string BundleID string } type DistributorBatchTemplateValues struct { LocationID string Batch string BatchRunID string BatchStartedDate string BundleID string } var distributorTemplateVariables = map[string]struct{}{ "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": {}, "storm_id": {}, } var distributorIdempotencyTemplateVariables = map[string]struct{}{ "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": {}, "storm_id": {}, "bundle_id": {}, } var distributorPipelineTemplateVariables = distributorIdempotencyTemplateVariables var distributorBatchTemplateVariables = map[string]struct{}{ "location_id": {}, "batch": {}, "batch_run_id": {}, "batch_started_date": {}, } var distributorBatchIdempotencyTemplateVariables = map[string]struct{}{ "location_id": {}, "batch": {}, "batch_run_id": {}, "batch_started_date": {}, "bundle_id": {}, } var distributorBatchPipelineTemplateVariables = distributorBatchTemplateVariables func RenderDistributorBundleID(template string, values DistributorTemplateValues) (string, error) { return renderDistributorTemplate("notify.distributor.bundle_id_template", template, values, distributorTemplateVariables) } func RenderDistributorPipelineID(template string, values DistributorTemplateValues) (string, error) { rendered, err := renderDistributorTemplate("notify.distributor.pipeline_id_template", template, values, distributorPipelineTemplateVariables) if err != nil { return "", err } if strings.TrimSpace(rendered) == "" { return "", fmt.Errorf("notify.distributor.pipeline_id_template renders an empty pipeline id") } return rendered, nil } func RenderDistributorIdempotencyKey(template string, values DistributorTemplateValues) (string, error) { return renderDistributorTemplate("notify.distributor.idempotency_key_template", template, values, distributorIdempotencyTemplateVariables) } func RenderDistributorBatchBundleID(template string, values DistributorBatchTemplateValues) (string, error) { rendered, err := renderDistributorBatchTemplate("notify.distributor.batch.bundle_id_template", template, values, distributorBatchTemplateVariables) if err != nil { return "", err } if strings.TrimSpace(rendered) == "" { return "", fmt.Errorf("notify.distributor.batch.bundle_id_template renders an empty bundle id") } return rendered, nil } func RenderDistributorBatchPipelineID(template string, values DistributorBatchTemplateValues) (string, error) { rendered, err := renderDistributorBatchTemplate("notify.distributor.batch.pipeline_id_template", template, values, distributorBatchPipelineTemplateVariables) if err != nil { return "", err } if strings.TrimSpace(rendered) == "" { return "", fmt.Errorf("notify.distributor.batch.pipeline_id_template renders an empty pipeline id") } return rendered, nil } func RenderDistributorBatchIdempotencyKey(template string, values DistributorBatchTemplateValues) (string, error) { rendered, err := renderDistributorBatchTemplate("notify.distributor.batch.idempotency_key_template", template, values, distributorBatchIdempotencyTemplateVariables) if err != nil { return "", err } if strings.TrimSpace(rendered) == "" { return "", fmt.Errorf("notify.distributor.batch.idempotency_key_template renders an empty idempotency key") } return rendered, nil } func RenderDistributorReportPaths(name string, templates []string, values DistributorTemplateValues) ([]string, error) { if len(templates) == 0 { 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 { itemName := fmt.Sprintf("%s[%d]", name, i) rendered, err := renderDistributorTemplate(itemName, template, values, distributorTemplateVariables) if err != nil { return nil, err } if err := ValidateDistributorReportPath(itemName, rendered); err != nil { return nil, err } if _, ok := seen[rendered]; ok { return nil, fmt.Errorf("%s renders duplicate path %q", name, rendered) } seen[rendered] = struct{}{} paths = append(paths, rendered) } return paths, nil } func validateDistributorTemplate(name, template string, allowed map[string]struct{}) error { _, err := renderDistributorTemplate(name, template, DistributorTemplateValues{}, allowed) return err } func validateDistributorBatchTemplate(name, template string, allowed map[string]struct{}) error { _, err := renderDistributorBatchTemplate(name, template, DistributorBatchTemplateValues{}, allowed) return err } func renderDistributorTemplate(name, template string, values DistributorTemplateValues, allowed map[string]struct{}) (string, error) { var rendered strings.Builder for i := 0; i < len(template); { switch template[i] { case '{': end := strings.IndexByte(template[i+1:], '}') if end < 0 { return "", fmt.Errorf("%s contains an unclosed template variable", name) } variable := template[i+1 : i+1+end] if variable == "" { return "", fmt.Errorf("%s contains an empty template variable", name) } if _, ok := allowed[variable]; !ok { return "", fmt.Errorf("%s contains unknown template variable %q", name, variable) } rendered.WriteString(distributorTemplateValue(variable, values)) i += end + 2 case '}': return "", fmt.Errorf("%s contains an unopened template variable", name) default: rendered.WriteByte(template[i]) i++ } } return rendered.String(), nil } func renderDistributorBatchTemplate(name, template string, values DistributorBatchTemplateValues, allowed map[string]struct{}) (string, error) { var rendered strings.Builder for i := 0; i < len(template); { switch template[i] { case '{': end := strings.IndexByte(template[i+1:], '}') if end < 0 { return "", fmt.Errorf("%s contains an unclosed template variable", name) } variable := template[i+1 : i+1+end] if variable == "" { return "", fmt.Errorf("%s contains an empty template variable", name) } if _, ok := allowed[variable]; !ok { return "", fmt.Errorf("%s contains unknown template variable %q", name, variable) } rendered.WriteString(distributorBatchTemplateValue(variable, values)) i += end + 2 case '}': return "", fmt.Errorf("%s contains an unopened template variable", name) default: rendered.WriteByte(template[i]) i++ } } return rendered.String(), nil } func distributorTemplateValue(variable string, values DistributorTemplateValues) string { switch variable { case "location_id": return values.LocationID case "report_id": return values.ReportID case "run_id": return values.RunID case "artifact_group": return values.ArtifactGroup case "batch_output_name": return values.BatchOutputName case "valid_start_date": return values.ValidStartDate case "valid_end_date": return values.ValidEndDate case "valid_start_time": return values.ValidStartTime case "valid_end_time": return values.ValidEndTime case "valid_start_stamp": return values.ValidStartStamp case "valid_end_stamp": return values.ValidEndStamp case "storm_id": return values.StormID case "bundle_id": return values.BundleID default: return "" } } func distributorBatchTemplateValue(variable string, values DistributorBatchTemplateValues) string { switch variable { case "location_id": return values.LocationID case "batch": return values.Batch case "batch_run_id": return values.BatchRunID case "batch_started_date": return values.BatchStartedDate case "bundle_id": return values.BundleID default: return "" } } func ValidateDistributorReportPath(name, path string) error { if path == "" { return fmt.Errorf("%s renders an empty path", name) } if isDistributorAbsolutePath(path) { return fmt.Errorf("%s must render a relative path", name) } if strings.Contains(path, "\\") { return fmt.Errorf("%s must not render backslashes", name) } segments := strings.Split(path, "/") for _, segment := range segments { if segment == "" { return fmt.Errorf("%s must not render empty path segments", name) } if segment == "." || segment == ".." { return fmt.Errorf("%s must not render . or .. path segments", name) } if segment == "manifest.json" || segment == ".distributor.json" { return fmt.Errorf("%s must not render reserved path segment %q", name, segment) } } return nil } func isDistributorAbsolutePath(path string) bool { if filepath.IsAbs(path) || strings.HasPrefix(path, "/") { return true } if len(path) >= 3 && isASCIIAlpha(path[0]) && path[1] == ':' && (path[2] == '/' || path[2] == '\\') { return true } return false } func isASCIIAlpha(ch byte) bool { return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') }