v0.32.0 53e25a3

mirror.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6	"context"
7	"errors"
8	"fmt"
9
10	pb "bnema.dev/ermite-api/gen/ermite"
11	"bnema.dev/ermite-api/gen/ermite/ermiteconnect"
12	"bnema.dev/ermite/internal/domain"
13	"bnema.dev/ermite/internal/ports"
14	"connectrpc.com/connect"
15)
16
17const FeatureMirror = "mirror"
18
19type mirrorService struct{ mirrors ports.MirrorService }
20
21var _ ermiteconnect.MirrorServiceHandler = (*mirrorService)(nil)
22
23func (s *mirrorService) GetMirrorKey(ctx context.Context, _ *connect.Request[pb.GetMirrorKeyRequest]) (*connect.Response[pb.GetMirrorKeyResponse], error) {
24	k, err := s.mirrors.ShowKey(ctx, caller(ctx))
25	if err != nil {
26		return nil, connectError(ctx, err)
27	}
28	return connect.NewResponse(&pb.GetMirrorKeyResponse{Key: mirrorKeyToPB(k)}), nil
29}
30func (s *mirrorService) RotateMirrorKey(ctx context.Context, _ *connect.Request[pb.RotateMirrorKeyRequest]) (*connect.Response[pb.RotateMirrorKeyResponse], error) {
31	r, err := s.mirrors.RotateKey(ctx, caller(ctx))
32	if err != nil {
33		return nil, connectError(ctx, err)
34	}
35	return connect.NewResponse(&pb.RotateMirrorKeyResponse{Key: mirrorKeyToPB(r.Key), PreviousFingerprint: r.Previous.Fingerprint}), nil
36}
37func (s *mirrorService) GetMirrorConfig(ctx context.Context, _ *connect.Request[pb.GetMirrorConfigRequest]) (*connect.Response[pb.GetMirrorConfigResponse], error) {
38	g, err := s.mirrors.GetGlobalConfig(ctx, caller(ctx))
39	if err != nil {
40		return nil, connectError(ctx, err)
41	}
42	return connect.NewResponse(&pb.GetMirrorConfigResponse{Config: mirrorConfigToPB(g)}), nil
43}
44func (s *mirrorService) SetMirrorConfig(ctx context.Context, req *connect.Request[pb.SetMirrorConfigRequest]) (*connect.Response[pb.SetMirrorConfigResponse], error) {
45	m := req.Msg
46	g, err := s.mirrors.SetGlobalConfig(ctx, caller(ctx), ports.MirrorGlobalPatch{Enabled: m.Enabled, URLTemplate: m.UrlTemplate, Cron: m.Cron, AllBranches: m.AllBranches})
47	if err != nil {
48		return nil, connectError(ctx, err)
49	}
50	return connect.NewResponse(&pb.SetMirrorConfigResponse{Config: mirrorConfigToPB(g)}), nil
51}
52func (s *mirrorService) SetRepoMirror(ctx context.Context, req *connect.Request[pb.SetRepoMirrorRequest]) (*connect.Response[pb.SetRepoMirrorResponse], error) {
53	m := req.Msg
54	name, err := domain.ParseRepoName(m.GetRepo())
55	if err != nil {
56		return nil, connectError(ctx, err)
57	}
58	t, err := s.mirrors.SetRepoMirror(ctx, caller(ctx), domain.MirrorRepoConfig{Repo: name, URL: m.GetUrl(), Cron: m.GetCron(), AllBranches: m.AllBranches})
59	if err != nil {
60		return nil, connectError(ctx, err)
61	}
62	return connect.NewResponse(&pb.SetRepoMirrorResponse{Target: mirrorTargetToPB(t)}), nil
63}
64func (s *mirrorService) RemoveRepoMirror(ctx context.Context, req *connect.Request[pb.RemoveRepoMirrorRequest]) (*connect.Response[pb.RemoveRepoMirrorResponse], error) {
65	name, err := domain.ParseRepoName(req.Msg.GetRepo())
66	if err != nil {
67		return nil, connectError(ctx, err)
68	}
69	if err := s.mirrors.RemoveRepoMirror(ctx, caller(ctx), name); err != nil {
70		return nil, connectError(ctx, err)
71	}
72	return connect.NewResponse(&pb.RemoveRepoMirrorResponse{}), nil
73}
74func (s *mirrorService) SetRepoMirrorExcluded(ctx context.Context, req *connect.Request[pb.SetRepoMirrorExcludedRequest]) (*connect.Response[pb.SetRepoMirrorExcludedResponse], error) {
75	name, err := domain.ParseRepoName(req.Msg.GetRepo())
76	if err != nil {
77		return nil, connectError(ctx, err)
78	}
79	t, err := s.mirrors.SetRepoExcluded(ctx, caller(ctx), name, req.Msg.GetExcluded())
80	if err != nil {
81		return nil, connectError(ctx, err)
82	}
83	return connect.NewResponse(&pb.SetRepoMirrorExcludedResponse{Target: mirrorTargetToPB(t)}), nil
84}
85func (s *mirrorService) GetMirrorStatus(ctx context.Context, req *connect.Request[pb.GetMirrorStatusRequest]) (*connect.Response[pb.GetMirrorStatusResponse], error) {
86	name, err := domain.ParseRepoName(req.Msg.GetRepo())
87	if err != nil {
88		return nil, connectError(ctx, err)
89	}
90	v, err := s.mirrors.GetStatus(ctx, caller(ctx), name)
91	if err != nil {
92		return nil, connectError(ctx, err)
93	}
94	return connect.NewResponse(&pb.GetMirrorStatusResponse{Status: mirrorStatusToPB(v)}), nil
95}
96func (s *mirrorService) ListMirrorStatus(ctx context.Context, _ *connect.Request[pb.ListMirrorStatusRequest]) (*connect.Response[pb.ListMirrorStatusResponse], error) {
97	list, err := s.mirrors.ListStatus(ctx, caller(ctx))
98	if err != nil {
99		return nil, connectError(ctx, err)
100	}
101	out := make([]*pb.MirrorStatus, 0, len(list))
102	for _, v := range list {
103		out = append(out, mirrorStatusToPB(v))
104	}
105	return connect.NewResponse(&pb.ListMirrorStatusResponse{Statuses: out}), nil
106}
107func (s *mirrorService) SyncMirror(ctx context.Context, req *connect.Request[pb.SyncMirrorRequest]) (*connect.Response[pb.SyncMirrorResponse], error) {
108	m := req.Msg
109	if m.GetAll() == (m.GetRepo() != "") {
110		return nil, connectError(ctx, fmt.Errorf("%w: specify either repo or all", domain.ErrInvalid))
111	}
112	var results []ports.MirrorSyncResult
113	if m.GetAll() {
114		list, err := s.mirrors.SyncAll(ctx, caller(ctx), m.GetForce())
115		if err != nil {
116			return nil, connectError(ctx, err)
117		}
118		results = list
119	} else {
120		name, err := domain.ParseRepoName(m.GetRepo())
121		if err != nil {
122			return nil, connectError(ctx, err)
123		}
124		result, err := s.mirrors.Sync(ctx, caller(ctx), name, m.GetForce())
125		if err != nil {
126			return nil, connectError(ctx, err)
127		}
128		result.Repo = name
129		results = []ports.MirrorSyncResult{result}
130	}
131	out := make([]*pb.SyncMirrorResult, 0, len(results))
132	for _, result := range results {
133		v, err := s.mirrors.GetStatus(ctx, caller(ctx), result.Repo)
134		if err != nil {
135			return nil, connectError(ctx, err)
136		}
137		if !result.Queued {
138			v.Status = result.Status
139		}
140		item := &pb.SyncMirrorResult{Repo: result.Repo.String(), Queued: result.Queued, NoOp: result.NoOp, Status: mirrorStatusToPB(v)}
141		if result.Err != nil {
142			item.Error = syncErrorText(result.Err)
143		}
144		out = append(out, item)
145	}
146	return connect.NewResponse(&pb.SyncMirrorResponse{Results: out}), nil
147}
148
149// syncErrorText is the short reason a repository failed in SyncAll: the failure class only.
150// The remote's output stays in GetMirrorStatus, so the response stays small and URL-free.
151func syncErrorText(err error) string {
152	var me *domain.MirrorError
153	if errors.As(err, &me) {
154		return "mirror " + string(me.Class) + ": see ermite mirror status"
155	}
156	switch {
157	case errors.Is(err, domain.ErrResourceExhausted):
158		return err.Error()
159	case errors.Is(err, domain.ErrNotFound):
160		return "repository not found"
161	case errors.Is(err, domain.ErrInvalid):
162		return "invalid mirror configuration"
163	}
164	return "sync failed"
165}
166
167func mirrorKeyToPB(k domain.MirrorPublicKey) *pb.MirrorKey {
168	return &pb.MirrorKey{AuthorizedKey: k.AuthorizedKey, Fingerprint: k.Fingerprint, CreatedAt: timestamp(k.CreatedAt)}
169}
170func mirrorConfigToPB(g domain.MirrorGlobalConfig) *pb.MirrorConfig {
171	return &pb.MirrorConfig{Enabled: g.Enabled, UrlTemplate: g.URLTemplate, Cron: g.Cron, AllBranches: g.AllBranches, UpdatedAt: timestamp(g.UpdatedAt)}
172}
173func mirrorStateToPB(s domain.MirrorState) pb.MirrorState {
174	switch s {
175	case domain.MirrorDisabled:
176		return pb.MirrorState_MIRROR_STATE_DISABLED
177	case domain.MirrorOK:
178		return pb.MirrorState_MIRROR_STATE_OK
179	case domain.MirrorFailing:
180		return pb.MirrorState_MIRROR_STATE_FAILING
181	case domain.MirrorPaused:
182		return pb.MirrorState_MIRROR_STATE_PAUSED
183	default:
184		return pb.MirrorState_MIRROR_STATE_UNSPECIFIED
185	}
186}
187func mirrorTargetToPB(t domain.MirrorTarget) *pb.MirrorTarget {
188	source := pb.MirrorSource_MIRROR_SOURCE_UNSPECIFIED
189	switch t.Source {
190	case domain.MirrorSourceNone:
191		source = pb.MirrorSource_MIRROR_SOURCE_DISABLED
192	case domain.MirrorSourceGlobal:
193		source = pb.MirrorSource_MIRROR_SOURCE_GLOBAL
194	case domain.MirrorSourceOverride:
195		source = pb.MirrorSource_MIRROR_SOURCE_OVERRIDE
196	case domain.MirrorSourceExcluded:
197		source = pb.MirrorSource_MIRROR_SOURCE_EXCLUDED
198	}
199	return &pb.MirrorTarget{Repo: t.Repo.String(), Enabled: t.Enabled, Url: t.URL, Cron: t.CronSpec, AllBranches: t.AllBranches, Source: source}
200}
201func mirrorStatusToPB(v ports.MirrorStatusView) *pb.MirrorStatus {
202	s := v.Status
203	return &pb.MirrorStatus{Target: mirrorTargetToPB(v.Target), State: mirrorStateToPB(s.State), FailureClass: string(s.Class), Reason: s.Reason, Detail: s.Detail, ConsecutiveFailures: int32(s.ConsecutiveFailures), LastAttemptAt: timestamp(s.LastAttemptAt), LastSuccessAt: timestamp(s.LastSuccessAt), NextRetryAt: timestamp(s.NextRetryAt), NextRunAt: timestamp(v.NextRunAt), PausedAt: timestamp(s.PausedAt)}
204}

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 2

Tags 45

↑↓ moveenter openesc close