Files
narratio/internal/config/validate_publish.go

156 lines
5.2 KiB
Go

package config
import (
"errors"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
type derivedPublishDestination struct {
index int
dest string
}
func deriveMissingPublishDestinations(cfg *PublishConfig, scriptorium *ScriptoriumConfig, notarius *NotariusConfig) []derivedPublishDestination {
if cfg == nil {
return nil
}
configuredOutputs := scriptoriumOutputPathMap(scriptorium)
extractionOutputs := notariusOutputKeySet(notarius)
derived := make([]derivedPublishDestination, 0)
for index, output := range cfg.Outputs {
if strings.TrimSpace(output.Dest) != "" {
continue
}
dest, err := artifactpolicy.ResolvePublishedDestinationWithExtractions(strings.TrimSpace(output.Source), "", configuredOutputs, extractionOutputs)
if err == nil {
derived = append(derived, derivedPublishDestination{index: index, dest: dest})
}
}
return derived
}
func applyMissingPublishDestinations(cfg *PublishConfig, scriptorium *ScriptoriumConfig, notarius *NotariusConfig) {
for _, derived := range deriveMissingPublishDestinations(cfg, scriptorium, notarius) {
cfg.Outputs[derived.index].Dest = derived.dest
}
}
func validateStorage(cfg StorageConfig) error {
backend := strings.ToLower(strings.TrimSpace(cfg.Backend))
switch backend {
case "", StorageBackendLocal:
if cfg.S3 != nil {
return fmt.Errorf("pipeline.storage.s3 is only supported when pipeline.storage.backend is s3")
}
return nil
case StorageBackendS3:
if cfg.S3 == nil {
return fmt.Errorf("pipeline.storage.s3 is required when pipeline.storage.backend is s3")
}
default:
return fmt.Errorf("pipeline.storage.backend must be one of: local, s3")
}
if strings.TrimSpace(cfg.S3.RootPrefix) == "" {
return fmt.Errorf("pipeline.storage.s3.root_prefix must be non-empty")
}
if err := validateRelativeSafePath("pipeline.storage.s3.root_prefix", cfg.S3.RootPrefix); err != nil {
return err
}
if cfg.S3.Endpoint != "" && strings.TrimSpace(cfg.S3.Endpoint) == "" {
return fmt.Errorf("pipeline.storage.s3.endpoint must be non-empty when provided")
}
if (cfg.S3.AccessKeyID == nil) != (cfg.S3.SecretAccessKey == nil) {
return fmt.Errorf("pipeline.storage.s3.access_key_id and pipeline.storage.s3.secret_access_key must be configured together")
}
return nil
}
func validatePublish(cfg *PublishConfig, scriptorium *ScriptoriumConfig, notarius *NotariusConfig) error {
if cfg == nil {
return nil
}
configuredOutputs := scriptoriumOutputPathMap(scriptorium)
extractionOutputs := notariusOutputKeySet(notarius)
seenDest := map[string]struct{}{}
for i, item := range cfg.Outputs {
prefix := fmt.Sprintf("pipeline.publish.outputs[%d]", i)
source := strings.TrimSpace(item.Source)
if source == "" {
return fmt.Errorf("%s.source is required", prefix)
}
if _, err := artifactpolicy.ValidatePublishSourceWithExtractions(source, configuredOutputs, extractionOutputs); err != nil {
return fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err)
}
dest := strings.TrimSpace(item.Dest)
if dest == "" {
return fmt.Errorf("%s.dest is required when destination cannot be derived from %q", prefix, source)
}
normalizedDest, err := pathsafe.NormalizeRelativeDestination(dest)
if err != nil {
switch {
case errors.Is(err, pathsafe.ErrRelativePathAbsolute):
return fmt.Errorf("%s.dest must be a relative path", prefix)
case errors.Is(err, pathsafe.ErrRelativePathEscape):
return fmt.Errorf("%s.dest must not contain path traversal", prefix)
default:
return fmt.Errorf("%s.dest must be non-empty", prefix)
}
}
if _, ok := seenDest[normalizedDest]; ok {
return fmt.Errorf("%s.dest %q duplicates another publish output destination", prefix, dest)
}
seenDest[normalizedDest] = struct{}{}
}
return nil
}
func validateCleanupPublishPolicy(cfg *PipelineConfig) error {
if cfg == nil || (!cfg.Spool.DeleteAudioAfterPublish && !cfg.Workspace.CleanupAfterPublish) {
return nil
}
if cfg.Publish == nil || (cfg.Publish.Enabled != nil && !*cfg.Publish.Enabled) {
return fmt.Errorf("cleanup stage policies require pipeline.publish.enabled=true")
}
if cfg.Publish.UploadRun != nil && !*cfg.Publish.UploadRun {
return fmt.Errorf("cleanup stage policies require pipeline.publish.upload_run=true")
}
return nil
}
func scriptoriumOutputPathMap(scriptorium *ScriptoriumConfig) map[string]string {
out := map[string]string{}
if scriptorium == nil || len(scriptorium.Artifacts) == 0 {
return out
}
for _, key := range sortedStringKeys(scriptorium.Artifacts) {
artifactCfg := scriptorium.Artifacts[key]
if !artifactpolicy.IsConfiguredKey(key) {
continue
}
out[strings.TrimSpace(key)] = strings.TrimSpace(artifactCfg.OutputPath)
}
return out
}
func publishUploadConfiguredForS3(pipeline *PipelineConfig) bool {
if pipeline == nil || pipeline.Publish == nil {
return false
}
if !strings.EqualFold(strings.TrimSpace(pipeline.Storage.Backend), StorageBackendS3) {
return false
}
enabled := true
if pipeline.Publish.Enabled != nil {
enabled = *pipeline.Publish.Enabled
}
upload := true
if pipeline.Publish.UploadRun != nil {
upload = *pipeline.Publish.UploadRun
}
return enabled && upload
}