1// Package imageref resolves image references to pinned digests without
2// pulling or running anything. Installation-registry refs resolve locally
3// through manifest storage; external refs resolve through the registry
4// transfer path (allowlist + require-digest policy enforced by callers).
5package imageref
6
7import (
8 "context"
9 "crypto/sha256"
10 "encoding/hex"
11 "fmt"
12 "strings"
13
14 "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/pkg/validation"
16)
17
18// ManifestReader is the manifest-storage subset the resolver needs.
19type ManifestReader interface {
20 GetManifest(name, reference string) ([]byte, string, error)
21}
22
23// RemoteResolver resolves external refs (allowlisted registries).
24type RemoteResolver interface {
25 ResolveDigest(ctx context.Context, ref string) (string, error)
26}
27
28// Resolver implements out.ImageResolver.
29type Resolver struct {
30 registryDomain string
31 manifests ManifestReader
32 remote RemoteResolver
33 policy domain.ImageSourcePolicy
34}
35
36// NewResolver wires local manifest resolution with an optional remote.
37func NewResolver(registryDomain string, manifests ManifestReader, remote RemoteResolver) *Resolver {
38 return &Resolver{
39 registryDomain: registryDomain,
40 manifests: manifests,
41 remote: remote,
42 policy: domain.ImageSourcePolicy{InstallationRegistry: registryDomain},
43 }
44}
45
46// WithPolicy installs the installation image-source policy. Every
47// reference, including an already-pinned digest, is validated against it
48// before resolution.
49func (r *Resolver) WithPolicy(policy domain.ImageSourcePolicy) *Resolver {
50 r.policy = policy
51 return r
52}
53
54// ResolveDigest implements out.ImageResolver.
55func (r *Resolver) ResolveDigest(ctx context.Context, ref string) (string, error) {
56 if err := ctx.Err(); err != nil {
57 return "", err
58 }
59 trimmed := strings.TrimSpace(ref)
60 if trimmed == "" {
61 return "", fmt.Errorf("imageref: empty reference: %w", domain.ErrAppImageUnresolvable)
62 }
63 // Already pinned: digest refs pass through after validation.
64 if _, digest, ok := strings.Cut(trimmed, "@"); ok {
65 if err := validation.ValidateImageDigest(digest); err != nil {
66 return "", fmt.Errorf("imageref: reference %q has invalid digest: %w", ref, domain.ErrAppImageUnresolvable)
67 }
68 }
69 // Every reference, pinned or not, is validated against installation
70 // policy before any resolution or pull.
71 if err := r.policy.ValidateImageSource(trimmed); err != nil {
72 return "", fmt.Errorf("imageref: %w", err)
73 }
74 if _, digest, ok := strings.Cut(trimmed, "@"); ok {
75 return digest, nil
76 }
77 // Installation-registry refs resolve locally.
78 if name, tag, ok := splitLocalRef(r.registryDomain, trimmed); ok {
79 raw, _, err := r.manifests.GetManifest(name, tag)
80 if err != nil {
81 return "", fmt.Errorf("imageref: local manifest %s:%s: %w", name, tag, domain.ErrAppImageUnresolvable)
82 }
83 sum := sha256.Sum256(raw)
84 return "sha256:" + hex.EncodeToString(sum[:]), nil
85 }
86 // External refs need the remote resolver.
87 if r.remote == nil {
88 return "", fmt.Errorf("imageref: external reference %q needs remote resolution: %w", ref, domain.ErrAppImageUnresolvable)
89 }
90 digest, err := r.remote.ResolveDigest(ctx, trimmed)
91 if err != nil {
92 return "", fmt.Errorf("imageref: remote %q: %w", ref, domain.ErrAppImageUnresolvable)
93 }
94 if err := validation.ValidateImageDigest(digest); err != nil {
95 return "", fmt.Errorf("imageref: remote %q returned invalid digest: %w", ref, domain.ErrAppImageUnresolvable)
96 }
97 return digest, nil
98}
99
100// splitLocalRef splits registry-domain refs into name + tag.
101func splitLocalRef(registryDomain, ref string) (string, string, bool) {
102 host, rest, ok := strings.Cut(ref, "/")
103 if !ok || host != registryDomain {
104 return "", "", false
105 }
106 name, tag, _ := strings.Cut(rest, ":")
107 if name == "" {
108 return "", "", false
109 }
110 if tag == "" {
111 tag = "latest"
112 }
113 return name, tag, true
114}