288 lines
11 KiB
Go
288 lines
11 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
|
)
|
|
|
|
type referenceTargetKey struct {
|
|
StepID string
|
|
LaneID string
|
|
Stage ModuleStage
|
|
}
|
|
|
|
func keyForReferenceTarget(target ResolvedReferenceTarget) referenceTargetKey {
|
|
return referenceTargetKey{StepID: target.StepID, LaneID: target.LaneID, Stage: target.Stage}
|
|
}
|
|
|
|
func operationReferenceSet(input RunInput, target ResolvedReferenceTarget) contracts.ReferenceSet {
|
|
if input.references != nil {
|
|
if set, ok := input.references[keyForReferenceTarget(target)]; ok {
|
|
return CloneReferenceSet(set)
|
|
}
|
|
}
|
|
return CloneReferenceSet(target.ReferenceSet)
|
|
}
|
|
|
|
// buildStepReferenceSets resolves every generated binding for a step before
|
|
// any lane in that step is allowed to start. Each returned set is a fresh
|
|
// operation-time view; prepared reference sets are never modified.
|
|
func buildStepReferenceSets(input RunInput, step PreparedPipelineStep, outputs []contracts.SerializedOutput) (map[referenceTargetKey]contracts.ReferenceSet, []artifacts.ReferenceProvenance, error) {
|
|
if len(step.lanes) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
sets := make(map[referenceTargetKey]contracts.ReferenceSet)
|
|
var provenance []artifacts.ReferenceProvenance
|
|
canonical := make(map[string]contracts.ReferenceItem)
|
|
for _, prepared := range step.lanes {
|
|
lane := prepared.resolved
|
|
for _, target := range []ResolvedReferenceTarget{lane.ExtractReferences, lane.MergeReferences, lane.NormalizeReferences} {
|
|
generated := false
|
|
resolved := CloneReferenceSet(target.ReferenceSet)
|
|
for _, binding := range target.Bindings {
|
|
if binding.Artifact == nil {
|
|
continue
|
|
}
|
|
generated = true
|
|
item, err := generatedReferenceItem(input, binding, outputs, canonical)
|
|
if err != nil {
|
|
return nil, nil, contextualHandoffError(input, target, binding, err)
|
|
}
|
|
slotName := strings.TrimSpace(binding.SlotName)
|
|
slot, ok := resolved.Slots[slotName]
|
|
if !ok {
|
|
return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q has no resolved declaration", slotName))
|
|
}
|
|
if len(slot.Items) > 0 {
|
|
return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q already contains materialized items", slotName))
|
|
}
|
|
if len(slot.Slot.AcceptedArtifactKinds) == 0 || !containsArtifactKind(slot.Slot.AcceptedArtifactKinds, item.ArtifactKind) {
|
|
return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q does not accept artifact kind %q", slotName, item.ArtifactKind))
|
|
}
|
|
if !referenceMediaTypeAccepted(item.MediaType, slot.Slot.AcceptedMediaTypes) {
|
|
return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q does not accept media type %q", slotName, item.MediaType))
|
|
}
|
|
if slot.Slot.MaxBytes > 0 && item.SizeBytes > slot.Slot.MaxBytes {
|
|
return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q is %d bytes, limit %d", slotName, item.SizeBytes, slot.Slot.MaxBytes))
|
|
}
|
|
slot.Items = []contracts.ReferenceItem{contracts.CloneReferenceItem(item)}
|
|
resolved.Slots[slotName] = slot
|
|
}
|
|
if !generated {
|
|
continue
|
|
}
|
|
key := keyForReferenceTarget(target)
|
|
sets[key] = resolved
|
|
for _, item := range generatedItems(resolved) {
|
|
provenance = append(provenance, referenceProvenanceForItem(target, item))
|
|
}
|
|
}
|
|
}
|
|
return sets, provenance, nil
|
|
}
|
|
|
|
func containsArtifactKind(kinds []contracts.ArtifactKind, want contracts.ArtifactKind) bool {
|
|
for _, kind := range kinds {
|
|
if kind == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func contextualHandoffError(input RunInput, target ResolvedReferenceTarget, binding ReferenceBinding, err error) error {
|
|
return fmt.Errorf("pipeline %q step %q lane %q %s generated dependency %q from %q/%q: %w", input.pipeline.ID, target.StepID, target.LaneID, target.Stage, binding.SlotName, binding.Artifact.Step, binding.Artifact.Lane, err)
|
|
}
|
|
|
|
func generatedReferenceItem(input RunInput, binding ReferenceBinding, outputs []contracts.SerializedOutput, cache map[string]contracts.ReferenceItem) (contracts.ReferenceItem, error) {
|
|
selector := binding.Artifact
|
|
if selector == nil {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("generated reference selector must not be nil")
|
|
}
|
|
stepID := strings.TrimSpace(selector.Step)
|
|
laneID := strings.TrimSpace(selector.Lane)
|
|
cacheKey := stepID + "\x00" + laneID
|
|
if item, ok := cache[cacheKey]; ok {
|
|
item.SlotName = strings.TrimSpace(binding.SlotName)
|
|
item.BindingSource = strings.TrimSpace(binding.BindingSource)
|
|
return contracts.CloneReferenceItem(item), nil
|
|
}
|
|
matches := make([]contracts.SerializedOutput, 0, 1)
|
|
for _, output := range outputs {
|
|
if strings.TrimSpace(output.StepID) == stepID && strings.TrimSpace(output.LaneID) == laneID {
|
|
matches = append(matches, output)
|
|
}
|
|
}
|
|
if len(matches) == 0 {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("producer has no accepted normalized output")
|
|
}
|
|
if len(matches) > 1 {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("producer has %d accepted normalized outputs; exactly one is required", len(matches))
|
|
}
|
|
producer, ok := findResolvedLane(input.pipeline, stepID, laneID)
|
|
if !ok {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("producer lane is not present in the resolved pipeline")
|
|
}
|
|
if input.Prepared == nil || input.Prepared.artifactCodecs == nil {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("artifact codec registry is unavailable")
|
|
}
|
|
serialized := contracts.CloneSerializedArtifact(matches[0].Artifact)
|
|
if serialized.Kind != producer.ArtifactKind {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("producer artifact kind %q does not match resolved kind %q", serialized.Kind, producer.ArtifactKind)
|
|
}
|
|
value, err := input.Prepared.artifactCodecs.Decode(serialized)
|
|
if err != nil {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("decode producer artifact: %w", err)
|
|
}
|
|
canonical, err := input.Prepared.artifactCodecs.Encode(producer.ArtifactKind, value)
|
|
if err != nil {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("canonicalize producer artifact: %w", err)
|
|
}
|
|
expected, ok := input.Prepared.artifactCodecs.Spec(producer.ArtifactKind)
|
|
if !ok {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("producer artifact codec %q is not registered", producer.ArtifactKind)
|
|
}
|
|
if canonical.Kind != expected.Kind || canonical.Schema.ID != expected.Schema.ID || canonical.Schema.Name != expected.Schema.Name || canonical.Schema.Version != expected.Schema.Version || contracts.DigestArtifactSchema(canonical.Schema) != expected.SchemaDigest || canonical.MediaType != expected.MediaType {
|
|
return contracts.ReferenceItem{}, fmt.Errorf("canonical producer artifact does not match registered codec identity")
|
|
}
|
|
item := contracts.ReferenceItem{
|
|
SlotName: strings.TrimSpace(binding.SlotName),
|
|
MediaType: canonical.MediaType,
|
|
Content: append([]byte(nil), canonical.Content...),
|
|
Digest: referenceDigest(canonical.Content),
|
|
Origin: contracts.ReferenceOrigin{Type: "generated"},
|
|
SizeBytes: int64(len(canonical.Content)),
|
|
BindingSource: strings.TrimSpace(binding.BindingSource),
|
|
ArtifactKind: canonical.Kind,
|
|
ArtifactSchema: contracts.CloneArtifactSchema(canonical.Schema),
|
|
Producer: contracts.ReferenceProducer{
|
|
PipelineID: input.pipeline.ID,
|
|
StepID: stepID,
|
|
LaneID: laneID,
|
|
ModuleKey: producer.Normalize.Module,
|
|
},
|
|
}
|
|
cacheItem := contracts.CloneReferenceItem(item)
|
|
cacheItem.SlotName = ""
|
|
cacheItem.BindingSource = ""
|
|
cache[cacheKey] = cacheItem
|
|
return item, nil
|
|
}
|
|
|
|
func findResolvedLane(pipeline ResolvedPipeline, stepID, laneID string) (ResolvedArtifactLane, bool) {
|
|
for _, step := range pipeline.Steps {
|
|
if step.ID != stepID {
|
|
continue
|
|
}
|
|
for _, lane := range step.ArtifactLanes {
|
|
if lane.ID == laneID {
|
|
return lane, true
|
|
}
|
|
}
|
|
}
|
|
return ResolvedArtifactLane{}, false
|
|
}
|
|
|
|
func generatedItems(set contracts.ReferenceSet) []contracts.ReferenceItem {
|
|
var items []contracts.ReferenceItem
|
|
keys := make([]string, 0, len(set.Slots))
|
|
for key := range set.Slots {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
for _, item := range set.Slots[key].Items {
|
|
if item.Origin.Type == "generated" {
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
}
|
|
return items
|
|
}
|
|
|
|
func referenceProvenanceForItem(target ResolvedReferenceTarget, item contracts.ReferenceItem) artifacts.ReferenceProvenance {
|
|
return artifacts.ReferenceProvenance{
|
|
Stage: string(target.Stage),
|
|
StepID: target.StepID,
|
|
LaneID: target.LaneID,
|
|
SlotName: item.SlotName,
|
|
OriginType: item.Origin.Type,
|
|
OriginURI: item.Origin.URI,
|
|
Digest: item.Digest,
|
|
MediaType: item.MediaType,
|
|
SizeBytes: item.SizeBytes,
|
|
BindingSource: item.BindingSource,
|
|
ArtifactKind: string(item.ArtifactKind),
|
|
SchemaID: item.ArtifactSchema.ID,
|
|
SchemaName: item.ArtifactSchema.Name,
|
|
SchemaVersion: item.ArtifactSchema.Version,
|
|
SchemaDigest: contracts.DigestArtifactSchema(item.ArtifactSchema),
|
|
ProducerPipeline: item.Producer.PipelineID,
|
|
ProducerStep: item.Producer.StepID,
|
|
ProducerLane: item.Producer.LaneID,
|
|
ProducerModule: item.Producer.ModuleKey,
|
|
}
|
|
}
|
|
|
|
type generatedReferenceFingerprintIdentity struct {
|
|
ProducerPipeline string `json:"producer_pipeline_id"`
|
|
ProducerStep string `json:"producer_step_id"`
|
|
ProducerLane string `json:"producer_lane_id"`
|
|
ProducerModule string `json:"producer_module_key"`
|
|
ArtifactKind string `json:"artifact_kind"`
|
|
SchemaID string `json:"schema_id"`
|
|
SchemaName string `json:"schema_name"`
|
|
SchemaVersion string `json:"schema_version"`
|
|
SchemaDigest string `json:"schema_digest"`
|
|
MediaType string `json:"media_type"`
|
|
ContentDigest string `json:"content_digest"`
|
|
}
|
|
|
|
// generatedReferenceDependencies returns the canonical semantic dependency
|
|
// identity that receiving operation checkpoints must include.
|
|
func generatedReferenceDependencies(set contracts.ReferenceSet) []CheckpointFingerprint {
|
|
var fingerprints []CheckpointFingerprint
|
|
keys := make([]string, 0, len(set.Slots))
|
|
for key := range set.Slots {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, slotName := range keys {
|
|
for index, item := range set.Slots[slotName].Items {
|
|
if item.Origin.Type != "generated" {
|
|
continue
|
|
}
|
|
identity := generatedReferenceFingerprintIdentity{
|
|
ProducerPipeline: item.Producer.PipelineID,
|
|
ProducerStep: item.Producer.StepID,
|
|
ProducerLane: item.Producer.LaneID,
|
|
ProducerModule: item.Producer.ModuleKey,
|
|
ArtifactKind: string(item.ArtifactKind),
|
|
SchemaID: item.ArtifactSchema.ID,
|
|
SchemaName: item.ArtifactSchema.Name,
|
|
SchemaVersion: item.ArtifactSchema.Version,
|
|
SchemaDigest: contracts.DigestArtifactSchema(item.ArtifactSchema),
|
|
MediaType: item.MediaType,
|
|
ContentDigest: item.Digest,
|
|
}
|
|
encoded, err := json.Marshal(identity)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sum := sha256.Sum256(encoded)
|
|
fingerprints = append(fingerprints, CheckpointFingerprint{
|
|
Name: fmt.Sprintf("generated-reference:%s:%d", slotName, index),
|
|
Value: "sha256:" + hex.EncodeToString(sum[:]),
|
|
})
|
|
}
|
|
}
|
|
return normalizeCheckpointFingerprints(fingerprints)
|
|
}
|