185 lines
6.5 KiB
Go
185 lines
6.5 KiB
Go
package checkpoint
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
const digestPrefixLength = 16
|
|
|
|
type Fingerprint struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
type IdentityInput struct {
|
|
Pipeline pipeline.ResolvedPipeline
|
|
InputKey, RawInputDigest, SourceDigest string
|
|
SelectedLanes []string
|
|
RuntimeOverrides []Fingerprint
|
|
References []artifacts.ReferenceProvenance
|
|
ProvenanceFingerprints []Fingerprint
|
|
}
|
|
type Identity struct {
|
|
Digest string `json:"digest"`
|
|
PipelineID string `json:"pipeline_id"`
|
|
PipelineDigest string `json:"pipeline_digest"`
|
|
InputKey string `json:"input_key"`
|
|
RawInputDigest string `json:"raw_input_digest,omitempty"`
|
|
SourceDigest string `json:"source_digest,omitempty"`
|
|
SelectedLanes []string `json:"selected_lanes,omitempty"`
|
|
RuntimeOverrides []Fingerprint `json:"runtime_overrides,omitempty"`
|
|
ReferenceDigests []Fingerprint `json:"reference_digests,omitempty"`
|
|
ProvenanceFingerprints []Fingerprint `json:"provenance_fingerprints,omitempty"`
|
|
}
|
|
|
|
func NewIdentity(input IdentityInput) (Identity, error) {
|
|
pipelineID, pipelineDigest, inputKey := strings.TrimSpace(input.Pipeline.ID), strings.TrimSpace(input.Pipeline.Digest), strings.TrimSpace(input.InputKey)
|
|
if pipelineID == "" {
|
|
return Identity{}, fmt.Errorf("checkpoint identity pipeline id must not be empty")
|
|
}
|
|
if pipelineDigest == "" {
|
|
return Identity{}, fmt.Errorf("checkpoint identity pipeline digest must not be empty")
|
|
}
|
|
if inputKey == "" {
|
|
inputKey = strings.TrimSpace(input.Pipeline.Input.Module)
|
|
}
|
|
if inputKey == "" {
|
|
return Identity{}, fmt.Errorf("checkpoint identity input key must not be empty")
|
|
}
|
|
if strings.TrimSpace(input.RawInputDigest) == "" && strings.TrimSpace(input.SourceDigest) == "" {
|
|
return Identity{}, fmt.Errorf("checkpoint identity raw input digest or source digest must be set")
|
|
}
|
|
v := Identity{PipelineID: pipelineID, PipelineDigest: pipelineDigest, InputKey: inputKey, RawInputDigest: strings.TrimSpace(input.RawInputDigest), SourceDigest: strings.TrimSpace(input.SourceDigest), SelectedLanes: normalizedLanes(input.SelectedLanes, input.Pipeline.ArtifactLanes), RuntimeOverrides: normalizeIdentityFingerprints(input.RuntimeOverrides), ReferenceDigests: referenceFingerprints(input.References), ProvenanceFingerprints: normalizeIdentityFingerprints(input.ProvenanceFingerprints)}
|
|
data, err := json.Marshal(Identity{PipelineID: v.PipelineID, PipelineDigest: v.PipelineDigest, InputKey: v.InputKey, RawInputDigest: v.RawInputDigest, SourceDigest: v.SourceDigest, SelectedLanes: v.SelectedLanes, RuntimeOverrides: v.RuntimeOverrides, ReferenceDigests: v.ReferenceDigests, ProvenanceFingerprints: v.ProvenanceFingerprints})
|
|
if err != nil {
|
|
return Identity{}, fmt.Errorf("marshal checkpoint identity: %w", err)
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
v.Digest = "sha256:" + hex.EncodeToString(sum[:])
|
|
return v, nil
|
|
}
|
|
func (i Identity) RelativePath() (string, error) {
|
|
p, err := safeComponent(i.PipelineID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("checkpoint identity pipeline id: %w", err)
|
|
}
|
|
k, err := safeComponent(i.InputKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("checkpoint identity input key: %w", err)
|
|
}
|
|
s := digestPrefix(i.SourceDigest)
|
|
if s == "" {
|
|
s = digestPrefix(i.RawInputDigest)
|
|
}
|
|
d := digestPrefix(i.PipelineDigest)
|
|
x := digestPrefix(i.Digest)
|
|
if s == "" || d == "" || x == "" {
|
|
return "", fmt.Errorf("checkpoint identity digest prefix must not be empty")
|
|
}
|
|
s, err = safeComponent(s)
|
|
if err != nil {
|
|
return "", fmt.Errorf("checkpoint identity source digest: %w", err)
|
|
}
|
|
d, err = safeComponent(d)
|
|
if err != nil {
|
|
return "", fmt.Errorf("checkpoint identity pipeline digest: %w", err)
|
|
}
|
|
x, err = safeComponent(x)
|
|
if err != nil {
|
|
return "", fmt.Errorf("checkpoint identity digest: %w", err)
|
|
}
|
|
return filepath.ToSlash(filepath.Join(p, k+"-"+s, d, x)), nil
|
|
}
|
|
func normalizedLanes(selected []string, resolved []pipeline.ResolvedArtifactLane) []string {
|
|
if len(selected) == 0 {
|
|
for _, lane := range resolved {
|
|
selected = append(selected, lane.ID)
|
|
}
|
|
}
|
|
return normalizeStrings(selected)
|
|
}
|
|
func normalizeIdentityFingerprints(values []Fingerprint) []Fingerprint {
|
|
by := map[string]string{}
|
|
for _, v := range values {
|
|
if n, x := strings.TrimSpace(v.Name), strings.TrimSpace(v.Value); n != "" && x != "" {
|
|
by[n] = x
|
|
}
|
|
}
|
|
names := make([]string, 0, len(by))
|
|
for n := range by {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
out := make([]Fingerprint, 0, len(names))
|
|
for _, n := range names {
|
|
out = append(out, Fingerprint{Name: n, Value: by[n]})
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
func referenceFingerprints(refs []artifacts.ReferenceProvenance) []Fingerprint {
|
|
var values []Fingerprint
|
|
for _, r := range refs {
|
|
if d := strings.TrimSpace(r.Digest); d != "" {
|
|
values = append(values, Fingerprint{Name: strings.Join([]string{strings.TrimSpace(r.Stage), strings.TrimSpace(r.LaneID), strings.TrimSpace(r.SlotName), strings.TrimSpace(r.OriginURI)}, ":"), Value: d})
|
|
}
|
|
}
|
|
return normalizeIdentityFingerprints(values)
|
|
}
|
|
func normalizeStrings(values []string) []string {
|
|
seen := map[string]struct{}{}
|
|
for _, v := range values {
|
|
if v = strings.TrimSpace(v); v != "" {
|
|
seen[v] = struct{}{}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for v := range seen {
|
|
out = append(out, v)
|
|
}
|
|
sort.Strings(out)
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
func digestPrefix(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if n := strings.Index(v, ":"); n >= 0 {
|
|
v = v[n+1:]
|
|
}
|
|
if len(v) > digestPrefixLength {
|
|
return v[:digestPrefixLength]
|
|
}
|
|
return v
|
|
}
|
|
func safeComponent(v string) (string, error) {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return "", fmt.Errorf("must not be empty")
|
|
}
|
|
var b strings.Builder
|
|
for _, r := range v {
|
|
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
|
|
b.WriteRune(r)
|
|
} else {
|
|
b.WriteString(fmt.Sprintf("~%x", r))
|
|
}
|
|
}
|
|
out := b.String()
|
|
if out == "." || out == ".." || strings.Contains(out, "..") || strings.ContainsAny(out, `/\\`) {
|
|
return "", fmt.Errorf("%q is not filesystem safe", v)
|
|
}
|
|
return out, nil
|
|
}
|