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}