247 lines
8.6 KiB
Go
247 lines
8.6 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/notariusref"
|
|
)
|
|
|
|
func validateNotarius(cfg *NotariusConfig, scriptorium *ScriptoriumConfig) error {
|
|
if cfg == nil || !cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
if strings.TrimSpace(cfg.Binary) == "" {
|
|
return fmt.Errorf("pipeline.notarius.binary is required when pipeline.notarius.enabled is true")
|
|
}
|
|
if strings.TrimSpace(cfg.ConfigPath) == "" {
|
|
return fmt.Errorf("pipeline.notarius.config_path is required when pipeline.notarius.enabled is true")
|
|
}
|
|
if strings.TrimSpace(cfg.PipelineID) == "" {
|
|
return fmt.Errorf("pipeline.notarius.pipeline_id is required when pipeline.notarius.enabled is true")
|
|
}
|
|
if strings.TrimSpace(cfg.Timeout) == "" {
|
|
return fmt.Errorf("pipeline.notarius.timeout must be a valid duration")
|
|
}
|
|
if err := validateDuration("pipeline.notarius.timeout", cfg.Timeout); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(cfg.WorkingDirectory) == "" {
|
|
return fmt.Errorf("pipeline.notarius.working_directory is required when pipeline.notarius.enabled is true")
|
|
}
|
|
if len(cfg.Outputs) == 0 {
|
|
return fmt.Errorf("pipeline.notarius.outputs must contain at least one output when pipeline.notarius.enabled is true")
|
|
}
|
|
if len(cfg.References) > MaxNotariusReferenceBindings {
|
|
return fmt.Errorf("pipeline.notarius.references must contain at most %d bindings", MaxNotariusReferenceBindings)
|
|
}
|
|
if err := validateNotariusCanonicalScalars(cfg); err != nil {
|
|
return err
|
|
}
|
|
if !validNotariusChunkCacheMode(cfg.ChunkCache) {
|
|
return fmt.Errorf("pipeline.notarius.chunk_cache must be empty, auto, bypass, or refresh")
|
|
}
|
|
if err := validateNotariusReferences(cfg.References); err != nil {
|
|
return err
|
|
}
|
|
return validateNotariusOutputs(cfg.Outputs, scriptorium)
|
|
}
|
|
|
|
func validateNotariusCanonicalScalars(cfg *NotariusConfig) error {
|
|
fields := []struct {
|
|
name string
|
|
got string
|
|
want string
|
|
}{
|
|
{name: "binary", got: cfg.Binary, want: strings.TrimSpace(cfg.Binary)},
|
|
{name: "chunk_cache", got: cfg.ChunkCache, want: strings.TrimSpace(cfg.ChunkCache)},
|
|
{name: "config_path", got: cfg.ConfigPath, want: cleanNotariusPath(cfg.ConfigPath)},
|
|
{name: "pipeline_id", got: cfg.PipelineID, want: strings.TrimSpace(cfg.PipelineID)},
|
|
{name: "timeout", got: cfg.Timeout, want: strings.TrimSpace(cfg.Timeout)},
|
|
{name: "working_directory", got: cfg.WorkingDirectory, want: cleanNotariusPath(cfg.WorkingDirectory)},
|
|
}
|
|
for _, field := range fields {
|
|
if field.got != field.want {
|
|
return fmt.Errorf("pipeline.notarius.%s must be normalized", field.name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validNotariusChunkCacheMode(mode string) bool {
|
|
switch mode {
|
|
case "", "auto", "bypass", "refresh":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func normalizeNotariusReferences(references map[string]string) (map[string]string, error) {
|
|
selectors := make([]string, 0, len(references))
|
|
for selector := range references {
|
|
selectors = append(selectors, selector)
|
|
}
|
|
sort.Strings(selectors)
|
|
|
|
var normalized map[string]string
|
|
if references != nil {
|
|
normalized = make(map[string]string, len(references))
|
|
}
|
|
owners := make(map[string]string, len(references))
|
|
for _, rawSelector := range selectors {
|
|
selector, err := notariusref.NormalizeSelector(rawSelector)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pipeline.notarius.references selector %q is invalid: %w", rawSelector, err)
|
|
}
|
|
if previous, ok := owners[selector]; ok {
|
|
return nil, fmt.Errorf("pipeline.notarius.references selectors %q and %q normalize to %q", previous, rawSelector, selector)
|
|
}
|
|
owners[selector] = rawSelector
|
|
|
|
source := strings.TrimSpace(references[rawSelector])
|
|
normalized[selector] = source
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func normalizeNotariusOutputs(outputs map[string]NotariusOutputConfig) (map[string]NotariusOutputConfig, error) {
|
|
keys := make([]string, 0, len(outputs))
|
|
for key := range outputs {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
normalized := make(map[string]NotariusOutputConfig, len(outputs))
|
|
sourceOwners := make(map[string]string, len(outputs))
|
|
for _, rawKey := range keys {
|
|
output := outputs[rawKey]
|
|
key := strings.TrimSpace(rawKey)
|
|
sourceID := artifactpolicy.ExtractionSourceID(key)
|
|
if previousKey, ok := sourceOwners[sourceID]; ok {
|
|
return nil, fmt.Errorf("pipeline.notarius.outputs keys %q and %q normalize to duplicate source %q", previousKey, rawKey, sourceID)
|
|
}
|
|
sourceOwners[sourceID] = rawKey
|
|
|
|
output.LaneID = strings.TrimSpace(output.LaneID)
|
|
output.MediaType = strings.TrimSpace(output.MediaType)
|
|
output.SchemaID = strings.TrimSpace(output.SchemaID)
|
|
output.SchemaVersion = strings.TrimSpace(output.SchemaVersion)
|
|
output.ModuleKey = strings.TrimSpace(output.ModuleKey)
|
|
normalized[key] = output
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func validateNotariusReferences(references map[string]string) error {
|
|
selectors := make([]string, 0, len(references))
|
|
for selector := range references {
|
|
selectors = append(selectors, selector)
|
|
}
|
|
sort.Strings(selectors)
|
|
for _, selector := range selectors {
|
|
canonical, err := notariusref.NormalizeSelector(selector)
|
|
if err != nil {
|
|
return fmt.Errorf("pipeline.notarius.references selector %q is invalid: %w", selector, err)
|
|
}
|
|
if canonical != selector {
|
|
return fmt.Errorf("pipeline.notarius.references selector %q must be normalized", selector)
|
|
}
|
|
source := references[selector]
|
|
if strings.TrimSpace(source) == "" {
|
|
return fmt.Errorf("pipeline.notarius.references.%s source is required", selector)
|
|
}
|
|
if source != strings.TrimSpace(source) {
|
|
return fmt.Errorf("pipeline.notarius.references.%s source must be normalized", selector)
|
|
}
|
|
if _, ok := artifactpolicy.DescribePreparedInputSource(source); !ok {
|
|
return fmt.Errorf("pipeline.notarius.references.%s source %q is not a supported prepared input source", selector, source)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNotariusOutputs(outputs map[string]NotariusOutputConfig, scriptorium *ScriptoriumConfig) error {
|
|
reservedSources := notariusReservedSources(scriptorium)
|
|
keys := make([]string, 0, len(outputs))
|
|
for key := range outputs {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
laneOwners := make(map[string]string, len(outputs))
|
|
for _, key := range keys {
|
|
output := outputs[key]
|
|
if strings.TrimSpace(key) != key || !artifactpolicy.IsConfiguredKey(key) {
|
|
return fmt.Errorf("pipeline.notarius.outputs keys must match ^[a-z][a-z0-9_]*$")
|
|
}
|
|
sourceID := artifactpolicy.ExtractionSourceID(key)
|
|
if owner, ok := reservedSources[sourceID]; ok {
|
|
return fmt.Errorf("pipeline.notarius.outputs.%s source %q collides with %s", key, sourceID, owner)
|
|
}
|
|
prefix := "pipeline.notarius.outputs." + key
|
|
fields := []struct{ name, value string }{
|
|
{name: "lane_id", value: output.LaneID},
|
|
{name: "media_type", value: output.MediaType},
|
|
{name: "schema_id", value: output.SchemaID},
|
|
{name: "schema_version", value: output.SchemaVersion},
|
|
{name: "module_key", value: output.ModuleKey},
|
|
}
|
|
for _, field := range fields {
|
|
if field.value != strings.TrimSpace(field.value) {
|
|
return fmt.Errorf("%s.%s must be normalized", prefix, field.name)
|
|
}
|
|
}
|
|
if output.LaneID == "" {
|
|
return fmt.Errorf("%s.lane_id is required", prefix)
|
|
}
|
|
if previousKey, ok := laneOwners[output.LaneID]; ok {
|
|
return fmt.Errorf("%s.lane_id %q duplicates pipeline.notarius.outputs.%s.lane_id", prefix, output.LaneID, previousKey)
|
|
}
|
|
laneOwners[output.LaneID] = key
|
|
if output.MediaType == "" {
|
|
return fmt.Errorf("%s.media_type is required", prefix)
|
|
}
|
|
if output.SchemaID == "" {
|
|
return fmt.Errorf("%s.schema_id is required", prefix)
|
|
}
|
|
if output.SchemaVersion == "" {
|
|
return fmt.Errorf("%s.schema_version is required", prefix)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func notariusReservedSources(scriptorium *ScriptoriumConfig) map[string]string {
|
|
reserved := make(map[string]string)
|
|
for _, spec := range artifactmodel.RuntimeTranscriptArtifacts() {
|
|
reserved[spec.SourceID] = "built-in source"
|
|
}
|
|
reserved[artifactpolicy.SourceBoundsSession] = "built-in source"
|
|
if scriptorium != nil {
|
|
for key := range scriptorium.Artifacts {
|
|
normalizedKey := strings.TrimSpace(key)
|
|
reserved[artifactpolicy.ConfiguredSourceID(normalizedKey)] = "configured Scriptorium source"
|
|
reserved[artifactpolicy.PreviousSessionSourceID(normalizedKey)] = "previous-session source"
|
|
}
|
|
}
|
|
return reserved
|
|
}
|
|
|
|
func notariusOutputKeySet(notarius *NotariusConfig) map[string]struct{} {
|
|
out := map[string]struct{}{}
|
|
if notarius == nil {
|
|
return out
|
|
}
|
|
for key := range notarius.Outputs {
|
|
trimmed := strings.TrimSpace(key)
|
|
if artifactpolicy.IsConfiguredKey(trimmed) {
|
|
out[trimmed] = struct{}{}
|
|
}
|
|
}
|
|
return out
|
|
}
|