v2.6.0 fe25b4f

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

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