Files
notarius/internal/framework/checkpoint/identity.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
}