179 lines
6.0 KiB
Go
179 lines
6.0 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
|
)
|
|
|
|
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 err := validateEnvVarNameField("pipeline.storage.s3.access_key_id_env", cfg.S3.AccessKeyIDEnv); err != nil {
|
|
return err
|
|
}
|
|
if err := validateEnvVarNameField("pipeline.storage.s3.secret_access_key_env", cfg.S3.SecretKeyEnv); err != nil {
|
|
return err
|
|
}
|
|
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 == "" {
|
|
derivedDest, err := artifactpolicy.ResolvePublishedDestinationWithExtractions(source, "", configuredOutputs, extractionOutputs)
|
|
if err != nil {
|
|
return fmt.Errorf("%s.dest is required when destination cannot be derived from %q: %w", prefix, source, err)
|
|
}
|
|
dest = derivedDest
|
|
cfg.Outputs[i].Dest = derivedDest
|
|
}
|
|
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{}{}
|
|
}
|
|
locks, err := ValidatePublishLockRules(cfg.Locks, scriptorium, notarius, "pipeline.publish.locks")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Locks = locks
|
|
return nil
|
|
}
|
|
|
|
// ValidatePublishLockRules validates and normalizes source-based publish locks.
|
|
func ValidatePublishLockRules(locks []PublishLockRule, scriptorium *ScriptoriumConfig, notarius *NotariusConfig, label string) ([]PublishLockRule, error) {
|
|
seenLocks := map[string]struct{}{}
|
|
out := make([]PublishLockRule, 0, len(locks))
|
|
configuredOutputs := scriptoriumOutputPathMap(scriptorium)
|
|
extractionOutputs := notariusOutputKeySet(notarius)
|
|
if strings.TrimSpace(label) == "" {
|
|
label = "publish.locks"
|
|
}
|
|
for i, item := range locks {
|
|
prefix := fmt.Sprintf("%s[%d]", label, i)
|
|
source := strings.TrimSpace(item.Source)
|
|
if source == "" {
|
|
return nil, fmt.Errorf("%s.source is required", prefix)
|
|
}
|
|
if _, err := artifactpolicy.ValidatePublishSourceWithExtractions(source, configuredOutputs, extractionOutputs); err != nil {
|
|
return nil, fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err)
|
|
}
|
|
if _, ok := seenLocks[source]; ok {
|
|
return nil, fmt.Errorf("%s.source %q duplicates another publish lock source", prefix, source)
|
|
}
|
|
seenLocks[source] = struct{}{}
|
|
out = append(out, PublishLockRule{
|
|
Source: source,
|
|
Reason: strings.TrimSpace(item.Reason),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// MergePublishLockRules returns the union of static and remote locks. Static
|
|
// locks win when both sources contain the same lock.
|
|
func MergePublishLockRules(staticLocks, remoteLocks []PublishLockRule) []PublishLockRule {
|
|
out := make([]PublishLockRule, 0, len(staticLocks)+len(remoteLocks))
|
|
seen := map[string]struct{}{}
|
|
for _, item := range staticLocks {
|
|
source := strings.TrimSpace(item.Source)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
|
|
seen[source] = struct{}{}
|
|
}
|
|
for _, item := range remoteLocks {
|
|
source := strings.TrimSpace(item.Source)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[source]; ok {
|
|
continue
|
|
}
|
|
out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
|
|
seen[source] = struct{}{}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func scriptoriumOutputPathMap(scriptorium *ScriptoriumConfig) map[string]string {
|
|
out := map[string]string{}
|
|
if scriptorium == nil || len(scriptorium.Artifacts) == 0 {
|
|
return out
|
|
}
|
|
for key, artifactCfg := range scriptorium.Artifacts {
|
|
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
|
|
}
|