v2.11.0 dca6537

service.go RawHistoryFolder

1// Package registry implements the container registry use case.
2package registry
3
4import (
5	"context"
6	"crypto/sha256"
7	"fmt"
8	"io"
9
10	"github.com/bnema/zerowrap"
11
12	"github.com/bnema/gordon/internal/boundaries/out"
13	"github.com/bnema/gordon/internal/domain"
14	"github.com/bnema/gordon/pkg/validation"
15)
16
17// Service implements the RegistryService interface.
18type Service struct {
19	blobStorage     out.BlobStorage
20	manifestStorage out.ManifestStorage
21	eventBus        out.EventPublisher
22}
23
24// NewService creates a new registry service.
25func NewService(
26	blobStorage out.BlobStorage,
27	manifestStorage out.ManifestStorage,
28	eventBus out.EventPublisher,
29) *Service {
30	return &Service{
31		blobStorage:     blobStorage,
32		manifestStorage: manifestStorage,
33		eventBus:        eventBus,
34	}
35}
36
37// GetManifest retrieves a manifest by name and reference.
38func (s *Service) GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error) {
39	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
40		zerowrap.FieldLayer:   "usecase",
41		zerowrap.FieldUseCase: "GetManifest",
42		"name":                name,
43		"reference":           reference,
44	})
45	log := zerowrap.FromCtx(ctx)
46
47	data, contentType, err := s.manifestStorage.GetManifest(name, reference)
48	if err != nil {
49		return nil, log.WrapErr(err, "failed to get manifest")
50	}
51
52	return &domain.Manifest{
53		Name:        name,
54		Reference:   reference,
55		ContentType: contentType,
56		Data:        data,
57	}, nil
58}
59
60// PutManifest stores a manifest and returns the calculated digest.
61func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
62	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
63		zerowrap.FieldLayer:   "usecase",
64		zerowrap.FieldUseCase: "PutManifest",
65		"name":                manifest.Name,
66		"reference":           manifest.Reference,
67	})
68	log := zerowrap.FromCtx(ctx)
69
70	// Calculate digest
71	digest := fmt.Sprintf("sha256:%x", sha256.Sum256(manifest.Data))
72
73	if err := s.manifestStorage.PutManifest(manifest.Name, manifest.Reference, manifest.ContentType, manifest.Data); err != nil {
74		return "", log.WrapErr(err, "failed to store manifest")
75	}
76
77	// Publish image pushed event
78	if s.eventBus != nil {
79		if err := s.eventBus.Publish(domain.EventImagePushed, domain.ImagePushedPayload{
80			Name:        manifest.Name,
81			Reference:   manifest.Reference,
82			Manifest:    manifest.Data,
83			Annotations: manifest.Annotations,
84		}); err != nil {
85			log.Warn().Err(err).Msg("failed to publish image pushed event")
86		}
87	}
88
89	log.Info().Str("digest", digest).Msg("manifest stored")
90	return digest, nil
91}
92
93// DeleteManifest removes a manifest.
94func (s *Service) DeleteManifest(ctx context.Context, name, reference string) error {
95	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
96		zerowrap.FieldLayer:   "usecase",
97		zerowrap.FieldUseCase: "DeleteManifest",
98		"name":                name,
99		"reference":           reference,
100	})
101	log := zerowrap.FromCtx(ctx)
102
103	if err := s.manifestStorage.DeleteManifest(name, reference); err != nil {
104		return log.WrapErr(err, "failed to delete manifest")
105	}
106
107	log.Info().Msg("manifest deleted")
108	return nil
109}
110
111// GetBlob retrieves a blob by digest.
112func (s *Service) GetBlob(ctx context.Context, digest string) (io.ReadCloser, error) {
113	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
114		zerowrap.FieldLayer:   "usecase",
115		zerowrap.FieldUseCase: "GetBlob",
116		"digest":              digest,
117	})
118	log := zerowrap.FromCtx(ctx)
119
120	reader, err := s.blobStorage.GetBlob(digest)
121	if err != nil {
122		return nil, log.WrapErr(err, "failed to get blob")
123	}
124
125	return reader, nil
126}
127
128// GetBlobPath returns the filesystem path to a blob for direct serving.
129func (s *Service) GetBlobPath(ctx context.Context, digest string) (string, error) {
130	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
131		zerowrap.FieldLayer:   "usecase",
132		zerowrap.FieldUseCase: "GetBlobPath",
133		"digest":              digest,
134	})
135	log := zerowrap.FromCtx(ctx)
136
137	path, err := s.blobStorage.GetBlobPath(digest)
138	if err != nil {
139		return "", log.WrapErr(err, "failed to get blob path")
140	}
141
142	return path, nil
143}
144
145// PutBlob stores a blob with the given digest.
146func (s *Service) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error {
147	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
148		zerowrap.FieldLayer:   "usecase",
149		zerowrap.FieldUseCase: "PutBlob",
150		"digest":              digest,
151		zerowrap.FieldSize:    size,
152	})
153	log := zerowrap.FromCtx(ctx)
154
155	if err := s.blobStorage.PutBlob(digest, data, size); err != nil {
156		return log.WrapErr(err, "failed to store blob")
157	}
158
159	log.Info().Msg("blob stored")
160	return nil
161}
162
163// BlobExists checks if a blob exists.
164func (s *Service) BlobExists(_ context.Context, digest string) bool {
165	return s.blobStorage.BlobExists(digest)
166}
167
168// StartUpload starts a new blob upload.
169func (s *Service) StartUpload(ctx context.Context, name string) (string, error) {
170	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
171		zerowrap.FieldLayer:   "usecase",
172		zerowrap.FieldUseCase: "StartUpload",
173		"name":                name,
174	})
175	log := zerowrap.FromCtx(ctx)
176
177	uuid, err := s.blobStorage.StartBlobUpload(name)
178	if err != nil {
179		return "", log.WrapErr(err, "failed to start blob upload")
180	}
181
182	log.Info().Str("uuid", uuid).Msg("blob upload started")
183	return uuid, nil
184}
185
186// AppendBlobChunk appends data to an in-progress blob upload.
187func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, chunk []byte) (int64, error) {
188	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
189		zerowrap.FieldLayer:   "usecase",
190		zerowrap.FieldUseCase: "AppendBlobChunk",
191		"name":                name,
192		"uuid":                uuid,
193		zerowrap.FieldSize:    len(chunk),
194	})
195	log := zerowrap.FromCtx(ctx)
196
197	length, err := s.blobStorage.AppendBlobChunk(name, uuid, chunk)
198	if err != nil {
199		return 0, log.WrapErr(err, "failed to append blob chunk")
200	}
201
202	return length, nil
203}
204
205// FinishUpload completes a blob upload.
206func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error {
207	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
208		zerowrap.FieldLayer:   "usecase",
209		zerowrap.FieldUseCase: "FinishUpload",
210		"uuid":                uuid,
211		"digest":              digest,
212	})
213	log := zerowrap.FromCtx(ctx)
214
215	if err := s.blobStorage.FinishBlobUpload(uuid, digest); err != nil {
216		return log.WrapErr(err, "failed to finish blob upload")
217	}
218
219	log.Info().Msg("blob upload finished")
220	return nil
221}
222
223// CancelUpload cancels an in-progress upload.
224func (s *Service) CancelUpload(ctx context.Context, uuid string) error {
225	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
226		zerowrap.FieldLayer:   "usecase",
227		zerowrap.FieldUseCase: "CancelUpload",
228		"uuid":                uuid,
229	})
230	log := zerowrap.FromCtx(ctx)
231
232	if err := s.blobStorage.CancelBlobUpload(uuid); err != nil {
233		return log.WrapErr(err, "failed to cancel blob upload")
234	}
235
236	log.Info().Msg("blob upload cancelled")
237	return nil
238}
239
240// ListTags returns all tags for a repository.
241func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) {
242	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
243		zerowrap.FieldLayer:   "usecase",
244		zerowrap.FieldUseCase: "ListTags",
245		"name":                name,
246	})
247	log := zerowrap.FromCtx(ctx)
248
249	tags, err := s.manifestStorage.ListTags(name)
250	if err != nil {
251		return nil, log.WrapErr(err, "failed to list tags")
252	}
253
254	filtered := make([]string, 0, len(tags))
255	for _, tag := range tags {
256		if validation.IsDigest(tag) {
257			continue
258		}
259		filtered = append(filtered, tag)
260	}
261
262	return filtered, nil
263}
264
265// ListRepositories returns all repository names.
266func (s *Service) ListRepositories(ctx context.Context) ([]string, error) {
267	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
268		zerowrap.FieldLayer:   "usecase",
269		zerowrap.FieldUseCase: "ListRepositories",
270	})
271	log := zerowrap.FromCtx(ctx)
272
273	repos, err := s.manifestStorage.ListRepositories()
274	if err != nil {
275		return nil, log.WrapErr(err, "failed to list repositories")
276	}
277
278	return repos, nil
279}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close