Make configured artifacts manifest authoritative
This commit is contained in:
134
internal/artifacts/analyze_evidence.go
Normal file
134
internal/artifacts/analyze_evidence.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||
)
|
||||
|
||||
// AnalyzeEvidenceState distinguishes a verified current configured artifact
|
||||
// from every form of unavailable evidence.
|
||||
type AnalyzeEvidenceState string
|
||||
|
||||
const (
|
||||
AnalyzeEvidenceCurrent AnalyzeEvidenceState = "current"
|
||||
AnalyzeEvidenceNonCurrent AnalyzeEvidenceState = "non_current"
|
||||
)
|
||||
|
||||
// AnalyzeEvidence is the read-only result of inspecting one configured
|
||||
// artifact's manifest record and durable output.
|
||||
type AnalyzeEvidence struct {
|
||||
State AnalyzeEvidenceState
|
||||
Reason string
|
||||
SourceID string
|
||||
Path string
|
||||
ProducerRunID string
|
||||
}
|
||||
|
||||
// InspectAnalyzeEvidence verifies that one configured artifact has supported,
|
||||
// current manifest evidence for the exact canonical bytes on disk.
|
||||
func InspectAnalyzeEvidence(
|
||||
paths SessionPaths,
|
||||
m *manifest.Manifest,
|
||||
key string,
|
||||
configured ConfiguredArtifactDefinition,
|
||||
) AnalyzeEvidence {
|
||||
normalizedKey := strings.TrimSpace(key)
|
||||
sourceID := ConfiguredArtifactSourceID(normalizedKey)
|
||||
nonCurrent := func(reason string) AnalyzeEvidence {
|
||||
return AnalyzeEvidence{State: AnalyzeEvidenceNonCurrent, Reason: reason, SourceID: sourceID}
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nonCurrent("analyze manifest evidence is absent; regenerate the artifact")
|
||||
}
|
||||
stageRecord := m.Stages["analyze"]
|
||||
if stageRecord == nil || stageRecord.Name != "analyze" {
|
||||
return nonCurrent("analyze manifest evidence is absent; regenerate the artifact")
|
||||
}
|
||||
if !stageRecord.HasVersionedAnalyzeState() {
|
||||
return nonCurrent("analyze manifest evidence is legacy or unsupported; regenerate the artifact")
|
||||
}
|
||||
record, ok := stageRecord.AnalyzeArtifacts[normalizedKey]
|
||||
if !ok {
|
||||
return nonCurrent("configured artifact has no analyze manifest record; regenerate the artifact")
|
||||
}
|
||||
if record.Status != manifest.AnalyzeArtifactCurrent {
|
||||
return nonCurrent(fmt.Sprintf("configured artifact manifest status is %q; regenerate the artifact", record.Status))
|
||||
}
|
||||
if err := manifest.ValidateAnalyzeArtifactCollection(
|
||||
stageRecord.AnalyzeStateVersion,
|
||||
map[string]manifest.AnalyzeArtifactRecord{normalizedKey: record},
|
||||
); err != nil {
|
||||
return nonCurrent("configured artifact manifest evidence is malformed; regenerate the artifact")
|
||||
}
|
||||
|
||||
configuredPath, err := pathsafe.NormalizeRelativeDestination(strings.TrimSpace(configured.OutputPath))
|
||||
if err != nil {
|
||||
return nonCurrent("configured artifact output path is unsafe; correct the configuration")
|
||||
}
|
||||
if record.Output.LocalPath != configuredPath {
|
||||
return nonCurrent("configured artifact manifest path differs from current configuration; regenerate the artifact")
|
||||
}
|
||||
if record.Output.SourceID != sourceID || record.Output.Kind != "scriptorium_artifact" {
|
||||
return nonCurrent("configured artifact manifest identity is incompatible; regenerate the artifact")
|
||||
}
|
||||
|
||||
file, err := fileops.OpenConfinedRegularFile(paths.Root, configuredPath)
|
||||
if err != nil {
|
||||
return nonCurrent("configured artifact output is missing or unsafe; regenerate the artifact")
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return nonCurrent("configured artifact output is not a safe regular file; regenerate the artifact")
|
||||
}
|
||||
hash := sha256.New()
|
||||
size, err := io.Copy(hash, file)
|
||||
if err != nil {
|
||||
return nonCurrent("configured artifact output could not be verified; regenerate the artifact")
|
||||
}
|
||||
if size != info.Size() || size != record.OutputSize {
|
||||
return nonCurrent("configured artifact output size differs from manifest evidence; regenerate the artifact")
|
||||
}
|
||||
if hex.EncodeToString(hash.Sum(nil)) != record.Output.Checksum {
|
||||
return nonCurrent("configured artifact output checksum differs from manifest evidence; regenerate the artifact")
|
||||
}
|
||||
|
||||
return AnalyzeEvidence{
|
||||
State: AnalyzeEvidenceCurrent,
|
||||
SourceID: sourceID,
|
||||
Path: filepath.Join(paths.Root, filepath.FromSlash(configuredPath)),
|
||||
ProducerRunID: record.ProducerRunID,
|
||||
}
|
||||
}
|
||||
|
||||
// HydrateAnalyzeArtifacts makes configured sources available only from
|
||||
// validated current manifest evidence. It never mutates manifest state.
|
||||
func (c *ArtifactCatalog) HydrateAnalyzeArtifacts(
|
||||
paths SessionPaths,
|
||||
m *manifest.Manifest,
|
||||
configured map[string]ConfiguredArtifactDefinition,
|
||||
) {
|
||||
if c == nil || len(configured) == 0 {
|
||||
return
|
||||
}
|
||||
for _, entry := range c.ListConfigured() {
|
||||
definition, ok := configured[entry.ConfiguredKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
evidence := InspectAnalyzeEvidence(paths, m, entry.ConfiguredKey, definition)
|
||||
if evidence.State != AnalyzeEvidenceCurrent {
|
||||
continue
|
||||
}
|
||||
_ = c.markAvailableFromAnalyzeManifest(evidence.SourceID, evidence.Path, evidence.ProducerRunID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user