registry.go
⎇
Raw
1package db
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7)
8
9// Manifest is one stored image manifest or index.
10type Manifest struct {
11 Digest string
12 MediaType string
13}
14
15// LinkBlob records that image (repo_id, image) references digest.
16func (d *DB) LinkBlob(ctx context.Context, repoID int64, image, digest string, size int64) error {
17 _, err := d.ExecContext(ctx,
18 `INSERT OR IGNORE INTO registry_blobs (repo_id, image, digest, size) VALUES (?, ?, ?, ?)`,
19 repoID, image, digest, size)
20 return err
21}
22
23// BlobLinked reports whether image references digest.
24func (d *DB) BlobLinked(ctx context.Context, repoID int64, image, digest string) (bool, error) {
25 var n int
26 err := d.QueryRowContext(ctx,
27 `SELECT COUNT(*) FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`,
28 repoID, image, digest).Scan(&n)
29 return n > 0, err
30}
31
32// UnlinkBlob drops one image's reference to digest.
33func (d *DB) UnlinkBlob(ctx context.Context, repoID int64, image, digest string) error {
34 _, err := d.ExecContext(ctx,
35 `DELETE FROM registry_blobs WHERE repo_id = ? AND image = ? AND digest = ?`,
36 repoID, image, digest)
37 return err
38}
39
40// DigestReferenced reports whether any image still uses the digest as a
41// blob or as a manifest. Blobs and manifest bodies share one file store, so
42// both tables count before a file is removed.
43func (d *DB) DigestReferenced(ctx context.Context, digest string) (bool, error) {
44 var n int
45 err := d.QueryRowContext(ctx,
46 `SELECT (SELECT COUNT(*) FROM registry_blobs WHERE digest = ?) +
47 (SELECT COUNT(*) FROM registry_manifests WHERE digest = ?)`,
48 digest, digest).Scan(&n)
49 return n > 0, err
50}
51
52// PutManifest stores the manifest row and, for a tag reference, points the
53// tag at it. It returns the digest the tag pointed at before, or "".
54func (d *DB) PutManifest(ctx context.Context, repoID int64, image string, m Manifest, tag, now string) (string, error) {
55 if _, err := d.ExecContext(ctx,
56 `INSERT OR REPLACE INTO registry_manifests (repo_id, image, digest, media_type, created_at)
57 VALUES (?, ?, ?, ?, ?)`,
58 repoID, image, m.Digest, m.MediaType, now); err != nil {
59 return "", err
60 }
61 if tag == "" {
62 return "", nil
63 }
64 var prev string
65 err := d.QueryRowContext(ctx,
66 `SELECT digest FROM registry_tags WHERE repo_id = ? AND image = ? AND tag = ?`,
67 repoID, image, tag).Scan(&prev)
68 if err != nil && !errors.Is(err, sql.ErrNoRows) {
69 return "", err
70 }
71 _, err = d.ExecContext(ctx,
72 `INSERT OR REPLACE INTO registry_tags (repo_id, image, tag, digest, updated_at) VALUES (?, ?, ?, ?, ?)`,
73 repoID, image, tag, m.Digest, now)
74 return prev, err
75}
76
77// ManifestByDigest returns nil when the image has no such manifest.
78func (d *DB) ManifestByDigest(ctx context.Context, repoID int64, image, digest string) (*Manifest, error) {
79 m := &Manifest{Digest: digest}
80 err := d.QueryRowContext(ctx,
81 `SELECT media_type FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`,
82 repoID, image, digest).Scan(&m.MediaType)
83 if errors.Is(err, sql.ErrNoRows) {
84 return nil, nil
85 }
86 return m, err
87}
88
89// ManifestByTag resolves a tag. It returns nil when the tag is unknown.
90func (d *DB) ManifestByTag(ctx context.Context, repoID int64, image, tag string) (*Manifest, error) {
91 m := &Manifest{}
92 err := d.QueryRowContext(ctx,
93 `SELECT m.digest, m.media_type FROM registry_tags t
94 JOIN registry_manifests m ON m.repo_id = t.repo_id AND m.image = t.image AND m.digest = t.digest
95 WHERE t.repo_id = ? AND t.image = ? AND t.tag = ?`,
96 repoID, image, tag).Scan(&m.Digest, &m.MediaType)
97 if errors.Is(err, sql.ErrNoRows) {
98 return nil, nil
99 }
100 return m, err
101}
102
103// DeleteManifest removes the manifest and every tag pointing at it. It
104// reports whether the manifest existed.
105func (d *DB) DeleteManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) {
106 if _, err := d.ExecContext(ctx,
107 `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND digest = ?`,
108 repoID, image, digest); err != nil {
109 return false, err
110 }
111 res, err := d.ExecContext(ctx,
112 `DELETE FROM registry_manifests WHERE repo_id = ? AND image = ? AND digest = ?`,
113 repoID, image, digest)
114 if err != nil {
115 return false, err
116 }
117 n, _ := res.RowsAffected()
118 return n > 0, nil
119}
120
121// DeleteTag removes one tag and returns the digest it pointed at. It
122// returns "" when the tag did not exist.
123func (d *DB) DeleteTag(ctx context.Context, repoID int64, image, tag string) (string, error) {
124 var digest string
125 err := d.QueryRowContext(ctx,
126 `DELETE FROM registry_tags WHERE repo_id = ? AND image = ? AND tag = ? RETURNING digest`,
127 repoID, image, tag).Scan(&digest)
128 if errors.Is(err, sql.ErrNoRows) {
129 return "", nil
130 }
131 return digest, err
132}
133
134// ListTags returns the image's tags in lexical order.
135func (d *DB) ListTags(ctx context.Context, repoID int64, image string) ([]string, error) {
136 return d.queryStrings(ctx,
137 `SELECT tag FROM registry_tags WHERE repo_id = ? AND image = ? ORDER BY tag`, repoID, image)
138}
139
140// ManifestDigests lists the manifest digests of one image.
141func (d *DB) ManifestDigests(ctx context.Context, repoID int64, image string) ([]string, error) {
142 return d.queryStrings(ctx,
143 `SELECT digest FROM registry_manifests WHERE repo_id = ? AND image = ?`, repoID, image)
144}
145
146// BlobDigests lists the blob digests linked to one image.
147func (d *DB) BlobDigests(ctx context.Context, repoID int64, image string) ([]string, error) {
148 return d.queryStrings(ctx,
149 `SELECT digest FROM registry_blobs WHERE repo_id = ? AND image = ?`, repoID, image)
150}
151
152// ImageRef names one image path of a repository.
153type ImageRef struct {
154 RepoID int64
155 Image string
156}
157
158// RegistryImages lists every image path that has a blob or a manifest.
159func (d *DB) RegistryImages(ctx context.Context) ([]ImageRef, error) {
160 rows, err := d.QueryContext(ctx,
161 `SELECT repo_id, image FROM registry_blobs UNION SELECT repo_id, image FROM registry_manifests`)
162 if err != nil {
163 return nil, err
164 }
165 defer rows.Close()
166 var out []ImageRef
167 for rows.Next() {
168 var r ImageRef
169 if err := rows.Scan(&r.RepoID, &r.Image); err != nil {
170 return nil, err
171 }
172 out = append(out, r)
173 }
174 return out, rows.Err()
175}
176
177func (d *DB) queryStrings(ctx context.Context, query string, args ...any) ([]string, error) {
178 rows, err := d.QueryContext(ctx, query, args...)
179 if err != nil {
180 return nil, err
181 }
182 defer rows.Close()
183 var out []string
184 for rows.Next() {
185 var v string
186 if err := rows.Scan(&v); err != nil {
187 return nil, err
188 }
189 out = append(out, v)
190 }
191 return out, rows.Err()
192}
193
194// ImageTag is one row of the Images tab.
195type ImageTag struct {
196 Image string
197 Tag string
198 Digest string
199 UpdatedAt string
200}
201
202// ListImageTags returns every tag of the repo, grouped by image path, the
203// most recently pushed tag first.
204func (d *DB) ListImageTags(ctx context.Context, repoID int64) ([]ImageTag, error) {
205 rows, err := d.QueryContext(ctx,
206 `SELECT image, tag, digest, updated_at FROM registry_tags WHERE repo_id = ?
207 ORDER BY image, updated_at DESC, tag`, repoID)
208 if err != nil {
209 return nil, err
210 }
211 defer rows.Close()
212 var out []ImageTag
213 for rows.Next() {
214 var t ImageTag
215 if err := rows.Scan(&t.Image, &t.Tag, &t.Digest, &t.UpdatedAt); err != nil {
216 return nil, err
217 }
218 out = append(out, t)
219 }
220 return out, rows.Err()
221}
222
223// ImageSizes sums the linked blob sizes per image path.
224func (d *DB) ImageSizes(ctx context.Context, repoID int64) (map[string]int64, error) {
225 rows, err := d.QueryContext(ctx,
226 `SELECT image, SUM(size) FROM registry_blobs WHERE repo_id = ? GROUP BY image`, repoID)
227 if err != nil {
228 return nil, err
229 }
230 defer rows.Close()
231 out := map[string]int64{}
232 for rows.Next() {
233 var image string
234 var size int64
235 if err := rows.Scan(&image, &size); err != nil {
236 return nil, err
237 }
238 out[image] = size
239 }
240 return out, rows.Err()
241}
242
243// DeleteUntaggedManifest removes the manifest when no tag of the image
244// points at it any more. It reports whether a row went.
245func (d *DB) DeleteUntaggedManifest(ctx context.Context, repoID int64, image, digest string) (bool, error) {
246 res, err := d.ExecContext(ctx,
247 `DELETE FROM registry_manifests WHERE repo_id = ?1 AND image = ?2 AND digest = ?3
248 AND NOT EXISTS (SELECT 1 FROM registry_tags WHERE repo_id = ?1 AND image = ?2 AND digest = ?3)`,
249 repoID, image, digest)
250 if err != nil {
251 return false, err
252 }
253 n, _ := res.RowsAffected()
254 return n > 0, nil
255}
256
257// RepoDigests lists every blob and manifest digest the repo references.
258// image narrows it to one image path; pass nil for the whole repo.
259func (d *DB) RepoDigests(ctx context.Context, repoID int64, image *string) ([]string, error) {
260 // A nil image binds as NULL and matches every row.
261 return d.queryStrings(ctx,
262 `SELECT digest FROM registry_blobs WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)
263 UNION SELECT digest FROM registry_manifests WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`,
264 repoID, image)
265}
266
267// DeleteImages removes the index rows of one image path, or of every image
268// when image is nil. Files are the caller's job.
269func (d *DB) DeleteImages(ctx context.Context, repoID int64, image *string) error {
270 for _, table := range []string{"registry_tags", "registry_manifests", "registry_blobs"} {
271 if _, err := d.ExecContext(ctx,
272 `DELETE FROM `+table+` WHERE repo_id = ?1 AND (?2 IS NULL OR image = ?2)`, repoID, image); err != nil {
273 return err
274 }
275 }
276 return nil
277}
278