1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.api;
18
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.collect.ImmutableList.toImmutableList;
21 import static com.linecorp.centraldogma.server.internal.mirror.DefaultMirroringServicePlugin.mirrorConfig;
22 import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.mirrorFile;
23 import static com.linecorp.centraldogma.server.mirror.MirrorUtil.validateMirrorId;
24
25 import java.net.URI;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.concurrent.CompletableFuture;
29 import java.util.concurrent.TimeUnit;
30
31 import org.jspecify.annotations.Nullable;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.cronutils.model.Cron;
36 import com.google.common.collect.ImmutableList;
37 import com.google.common.collect.ImmutableMap;
38
39 import com.linecorp.armeria.server.annotation.ConsumesJson;
40 import com.linecorp.armeria.server.annotation.Delete;
41 import com.linecorp.armeria.server.annotation.Get;
42 import com.linecorp.armeria.server.annotation.Param;
43 import com.linecorp.armeria.server.annotation.Post;
44 import com.linecorp.armeria.server.annotation.ProducesJson;
45 import com.linecorp.armeria.server.annotation.Put;
46 import com.linecorp.armeria.server.annotation.StatusCode;
47 import com.linecorp.armeria.server.annotation.decorator.RequestTimeout;
48 import com.linecorp.centraldogma.common.Author;
49 import com.linecorp.centraldogma.common.Change;
50 import com.linecorp.centraldogma.common.Markup;
51 import com.linecorp.centraldogma.common.ProjectRole;
52 import com.linecorp.centraldogma.common.RepositoryRole;
53 import com.linecorp.centraldogma.common.Revision;
54 import com.linecorp.centraldogma.internal.api.v1.MirrorDto;
55 import com.linecorp.centraldogma.internal.api.v1.MirrorRequest;
56 import com.linecorp.centraldogma.internal.api.v1.PushResultDto;
57 import com.linecorp.centraldogma.server.CentralDogmaConfig;
58 import com.linecorp.centraldogma.server.ZoneConfig;
59 import com.linecorp.centraldogma.server.command.Command;
60 import com.linecorp.centraldogma.server.command.CommandExecutor;
61 import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
62 import com.linecorp.centraldogma.server.internal.api.auth.RequiresRepositoryRole;
63 import com.linecorp.centraldogma.server.internal.mirror.MirrorRunner;
64 import com.linecorp.centraldogma.server.internal.mirror.MirrorSchedulingService;
65 import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
66 import com.linecorp.centraldogma.server.metadata.User;
67 import com.linecorp.centraldogma.server.mirror.Mirror;
68 import com.linecorp.centraldogma.server.mirror.MirrorAccessController;
69 import com.linecorp.centraldogma.server.mirror.MirrorListener;
70 import com.linecorp.centraldogma.server.mirror.MirrorResult;
71 import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig;
72 import com.linecorp.centraldogma.server.storage.repository.MetaRepository;
73 import com.linecorp.centraldogma.server.storage.repository.Repository;
74
75
76
77
78 @ProducesJson
79 public class MirroringServiceV1 extends AbstractService {
80
81 private static final Logger logger = LoggerFactory.getLogger(MirroringServiceV1.class);
82
83
84
85
86
87 private final ProjectApiManager projectApiManager;
88 private final MirrorRunner mirrorRunner;
89 private final Map<String, Object> mirrorZoneConfig;
90 @Nullable
91 private final ZoneConfig zoneConfig;
92 private final MirrorAccessController accessController;
93
94 public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor,
95 MirrorRunner mirrorRunner, CentralDogmaConfig config,
96 MirrorAccessController accessController) {
97 super(executor);
98 this.projectApiManager = projectApiManager;
99 this.mirrorRunner = mirrorRunner;
100 zoneConfig = config.zone();
101 mirrorZoneConfig = mirrorZoneConfig(config);
102 this.accessController = accessController;
103 }
104
105 private static Map<String, Object> mirrorZoneConfig(CentralDogmaConfig config) {
106 final MirroringServicePluginConfig mirrorConfig = mirrorConfig(config);
107 final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builderWithExpectedSize(2);
108 final boolean zonePinned = mirrorConfig != null && mirrorConfig.zonePinned();
109 builder.put("zonePinned", zonePinned);
110 final ZoneConfig zone = config.zone();
111 if (zone != null) {
112 builder.put("zone", zone);
113 }
114 return builder.build();
115 }
116
117
118
119
120
121
122 @RequiresProjectRole(ProjectRole.OWNER)
123 @Get("/projects/{projectName}/mirrors")
124 public CompletableFuture<List<MirrorDto>> listProjectMirrors(@Param String projectName) {
125 final CompletableFuture<List<Mirror>> future = metaRepo(projectName).mirrors(true);
126 return convertToMirrorDtos(projectName, future);
127 }
128
129
130
131
132
133
134 @RequiresRepositoryRole(RepositoryRole.ADMIN)
135 @Get("/projects/{projectName}/repos/{repoName}/mirrors")
136 public CompletableFuture<List<MirrorDto>> listRepoMirrors(@Param String projectName,
137 Repository repository) {
138 final CompletableFuture<List<Mirror>> future = metaRepo(projectName).mirrors(repository.name(), true);
139 return convertToMirrorDtos(projectName, future);
140 }
141
142
143
144
145
146
147 @RequiresRepositoryRole(RepositoryRole.ADMIN)
148 @Get("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
149 public CompletableFuture<MirrorDto> getMirror(@Param String projectName,
150 Repository repository,
151 @Param String id) {
152 return metaRepo(projectName).mirror(repository.name(), id).thenCompose(mirror -> {
153 return accessController.isAllowed(mirror.remoteRepoUri()).thenApply(allowed -> {
154 return convertToMirrorDto(projectName, mirror, allowed);
155 });
156 });
157 }
158
159
160
161
162
163
164 @Post("/projects/{projectName}/repos/{repoName}/mirrors")
165 @ConsumesJson
166 @StatusCode(201)
167 @RequiresRepositoryRole(RepositoryRole.ADMIN)
168 public CompletableFuture<PushResultDto> createMirror(@Param String projectName,
169 Repository repository,
170 MirrorRequest newMirror,
171 Author author, User user) {
172 validateMirrorId(newMirror.id());
173 return createOrUpdate(projectName, repository.name(), newMirror, author, user, false);
174 }
175
176
177
178
179
180
181 @ConsumesJson
182 @Put("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
183 @RequiresRepositoryRole(RepositoryRole.ADMIN)
184 public CompletableFuture<PushResultDto> updateMirror(@Param String projectName,
185 Repository repository,
186 MirrorRequest mirror,
187 @Param String id, Author author, User user) {
188 checkArgument(id.equals(mirror.id()), "The mirror ID (%s) can't be updated", id);
189 return createOrUpdate(projectName, repository.name(), mirror, author, user, true);
190 }
191
192
193
194
195
196
197 @Delete("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
198 @RequiresRepositoryRole(RepositoryRole.ADMIN)
199 public CompletableFuture<Void> deleteMirror(@Param String projectName,
200 Repository repository,
201 @Param String id, Author author) {
202 final MetaRepository metaRepository = metaRepo(projectName);
203 final String repoName = repository.name();
204 return metaRepository.mirror(repoName, id).thenCompose(mirror -> {
205
206 final Command<Revision> command =
207 Command.push(author, projectName, metaRepository.name(),
208 Revision.HEAD, "Delete mirror: " + id + " in " + repoName, "",
209 Markup.PLAINTEXT, Change.ofRemoval(mirrorFile(repoName, id)));
210 return executor().execute(command).thenApply(result -> null);
211 });
212 }
213
214 private CompletableFuture<PushResultDto> createOrUpdate(
215 String projectName, String repoName, MirrorRequest newMirror,
216 Author author, User user, boolean update) {
217 final MetaRepository metaRepo = metaRepo(projectName);
218
219 return metaRepo.createMirrorPushCommand(repoName, newMirror, author, zoneConfig, update).thenCompose(
220 command -> {
221 return executor().execute(command).thenApply(revision -> {
222 metaRepo.mirror(repoName, newMirror.id(), revision)
223 .handle((mirror, cause) -> {
224 if (cause != null) {
225
226 logger.warn("Failed to get the mirror: {}", newMirror.id(), cause);
227 return null;
228 }
229 return notifyMirrorEvent(mirror, user, update);
230 });
231 return new PushResultDto(revision, command.timestamp());
232 });
233 });
234 }
235
236 private Void notifyMirrorEvent(Mirror mirror, User user, boolean update) {
237 try {
238 final MirrorListener listener = MirrorSchedulingService.mirrorListener();
239 if (update) {
240 listener.onUpdate(mirror, user, accessController);
241 } else {
242 listener.onCreate(mirror, user, accessController);
243 }
244 } catch (Throwable ex) {
245 logger.warn("Failed to notify the mirror listener. (mirror: {})", mirror, ex);
246 }
247 return null;
248 }
249
250
251
252
253
254
255
256 @RequestTimeout(value = 5, unit = TimeUnit.MINUTES)
257 @Post("/projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run")
258 @RequiresRepositoryRole(RepositoryRole.ADMIN)
259 public CompletableFuture<MirrorResult> runMirror(@Param String projectName,
260 Repository repository,
261 @Param String mirrorId,
262 User user) throws Exception {
263 return mirrorRunner.run(projectName, repository.name(), mirrorId, user);
264 }
265
266
267
268
269
270
271 @Get("/mirror/config")
272 public Map<String, Object> config() {
273
274 return mirrorZoneConfig;
275 }
276
277 private CompletableFuture<List<MirrorDto>> convertToMirrorDtos(
278 String projectName, CompletableFuture<List<Mirror>> future) {
279 return future.thenCompose(mirrors -> {
280 final ImmutableList<String> remoteUris = mirrors.stream().map(
281 mirror -> mirror.remoteRepoUri().toString()).collect(
282 toImmutableList());
283 return accessController.isAllowed(remoteUris).thenApply(acl -> {
284 return mirrors.stream()
285 .map(mirror -> convertToMirrorDto(projectName, mirror, acl))
286 .collect(toImmutableList());
287 });
288 });
289 }
290
291 private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, Map<String, Boolean> acl) {
292 final boolean allowed = acl.get(mirror.remoteRepoUri().toString());
293 return convertToMirrorDto(projectName, mirror, allowed);
294 }
295
296 private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, boolean allowed) {
297 final URI remoteRepoUri = mirror.remoteRepoUri();
298 final Cron schedule = mirror.schedule();
299 final String scheduleStr = schedule != null ? schedule.asString() : null;
300 return new MirrorDto(mirror.id(),
301 mirror.enabled(), projectName,
302 scheduleStr,
303 mirror.direction().name(),
304 mirror.localRepo().name(),
305 mirror.localPath(),
306 remoteRepoUri.getScheme(),
307 remoteRepoUri.getAuthority() + remoteRepoUri.getPath(),
308 mirror.remotePath(),
309 mirror.remoteBranch(),
310 mirror.gitignore(),
311 mirror.credential().name(), mirror.zone(), allowed);
312 }
313
314 private MetaRepository metaRepo(String projectName) {
315 return projectApiManager.getProject(projectName).metaRepo();
316 }
317 }