156 lines
5.2 KiB
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
|
|
}
|