Harden pipeline state and plan release upgrades
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@@ -77,7 +79,14 @@ func recomputePipelineEffectiveDigest(cfg *PipelineConfig) error {
|
||||
if cfg == nil || cfg.resolution == nil {
|
||||
return fmt.Errorf("pipeline resolution metadata is required")
|
||||
}
|
||||
data, err := yaml.Marshal(cfg)
|
||||
digestConfig := *cfg
|
||||
if cfg.Notarius != nil && cfg.resolution.logicalNotariusCaptured {
|
||||
notarius := *cfg.Notarius
|
||||
notarius.ConfigPath = cfg.resolution.logicalNotariusConfig
|
||||
notarius.WorkingDirectory = cfg.resolution.logicalNotariusWorking
|
||||
digestConfig.Notarius = ¬arius
|
||||
}
|
||||
data, err := yaml.Marshal(&digestConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("serialize normalized effective pipeline: %w", err)
|
||||
}
|
||||
@@ -93,3 +102,28 @@ func recomputePipelineEffectiveDigest(cfg *PipelineConfig) error {
|
||||
cfg.resolution.effectiveDigest = hex.EncodeToString(digest[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// captureLogicalNotariusPaths retains normalized user-facing path semantics
|
||||
// before runtime resolution makes relative paths depend on the checkout or
|
||||
// installation directory. Runtime paths remain absolute; provenance does not.
|
||||
func captureLogicalNotariusPaths(cfg *PipelineConfig) {
|
||||
if cfg == nil || cfg.resolution == nil || cfg.Notarius == nil {
|
||||
return
|
||||
}
|
||||
configPath := normalizeLogicalFilesystemPath(cfg.Notarius.ConfigPath)
|
||||
workingDirectory := normalizeLogicalFilesystemPath(cfg.Notarius.WorkingDirectory)
|
||||
if cfg.Notarius.Enabled && workingDirectory == "" && configPath != "" {
|
||||
workingDirectory = normalizeLogicalFilesystemPath(filepath.Dir(configPath))
|
||||
}
|
||||
cfg.resolution.logicalNotariusConfig = configPath
|
||||
cfg.resolution.logicalNotariusWorking = workingDirectory
|
||||
cfg.resolution.logicalNotariusCaptured = true
|
||||
}
|
||||
|
||||
func normalizeLogicalFilesystemPath(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(filepath.Clean(value))
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ func LoadPipelineProfilePair(path, leftProfile, rightProfile string) (*PipelineC
|
||||
func finalizeLoadedPipeline(path string, cfg *PipelineConfig) (*PipelineConfig, error) {
|
||||
cfg.resolution.publishDeclared = cfg.Publish != nil
|
||||
applyPipelineDefaults(cfg)
|
||||
captureLogicalNotariusPaths(cfg)
|
||||
if err := resolveNotariusPaths(cfg, path); err != nil {
|
||||
return nil, fmt.Errorf("load pipeline config: %w", err)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,36 @@ notarius:
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotariusRelativePathsDoNotMakeEffectiveDigestLocationDependent(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
notarius:
|
||||
enabled: true
|
||||
config_path: notarius/config.yml
|
||||
pipeline_id: dnd-session
|
||||
`
|
||||
paths := make([]string, 2)
|
||||
configs := make([]*PipelineConfig, 2)
|
||||
for index := range paths {
|
||||
dir := t.TempDir()
|
||||
paths[index] = filepath.Join(dir, "pipeline.yml")
|
||||
if err := os.WriteFile(paths[index], []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline %d: %v", index, err)
|
||||
}
|
||||
loaded, err := LoadPipeline(paths[index])
|
||||
if err != nil {
|
||||
t.Fatalf("LoadPipeline(%d) error = %v", index, err)
|
||||
}
|
||||
configs[index] = loaded
|
||||
}
|
||||
if configs[0].Notarius.ConfigPath == configs[1].Notarius.ConfigPath ||
|
||||
configs[0].Notarius.WorkingDirectory == configs[1].Notarius.WorkingDirectory {
|
||||
t.Fatalf("runtime Notarius paths should remain location-specific: %#v / %#v", configs[0].Notarius, configs[1].Notarius)
|
||||
}
|
||||
if first, second := EffectivePipelineDigest(configs[0]), EffectivePipelineDigest(configs[1]); first == "" || first != second {
|
||||
t.Fatalf("relocated logical configuration digests = %q / %q, want equal non-empty values", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotariusStrictYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -18,6 +18,9 @@ type pipelineResolutionMetadata struct {
|
||||
sources []string
|
||||
selectedProfile *pipelineProfileSelection
|
||||
effectiveDigest string
|
||||
logicalNotariusConfig string
|
||||
logicalNotariusWorking string
|
||||
logicalNotariusCaptured bool
|
||||
ownership []pipelineFieldOwnership
|
||||
artifactFamilies map[string]ScriptoriumArtifactFamilyConfig
|
||||
artifactFamiliesExpanded bool
|
||||
|
||||
Reference in New Issue
Block a user