v2.15.1 c808592

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

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