1   /*
2    * Copyright 2023 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
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  
24  import java.net.URI;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.CompletableFuture;
28  import java.util.concurrent.TimeUnit;
29  
30  import javax.annotation.Nullable;
31  
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.command.CommitResult;
62  import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
63  import com.linecorp.centraldogma.server.internal.api.auth.RequiresRepositoryRole;
64  import com.linecorp.centraldogma.server.internal.mirror.MirrorRunner;
65  import com.linecorp.centraldogma.server.internal.mirror.MirrorSchedulingService;
66  import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
67  import com.linecorp.centraldogma.server.metadata.User;
68  import com.linecorp.centraldogma.server.mirror.Mirror;
69  import com.linecorp.centraldogma.server.mirror.MirrorAccessController;
70  import com.linecorp.centraldogma.server.mirror.MirrorListener;
71  import com.linecorp.centraldogma.server.mirror.MirrorResult;
72  import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig;
73  import com.linecorp.centraldogma.server.storage.repository.MetaRepository;
74  import com.linecorp.centraldogma.server.storage.repository.Repository;
75  
76  /**
77   * Annotated service object for managing mirroring service.
78   */
79  @ProducesJson
80  public class MirroringServiceV1 extends AbstractService {
81  
82      private static final Logger logger = LoggerFactory.getLogger(MirroringServiceV1.class);
83  
84      // TODO(ikhoon):
85      //  - Write documentation for the REST API specification
86      //  - Add Java APIs to the CentralDogma client
87  
88      private final ProjectApiManager projectApiManager;
89      private final MirrorRunner mirrorRunner;
90      private final Map<String, Object> mirrorZoneConfig;
91      @Nullable
92      private final ZoneConfig zoneConfig;
93      private final MirrorAccessController accessController;
94  
95      public MirroringServiceV1(ProjectApiManager projectApiManager, CommandExecutor executor,
96                                MirrorRunner mirrorRunner, CentralDogmaConfig config,
97                                MirrorAccessController accessController) {
98          super(executor);
99          this.projectApiManager = projectApiManager;
100         this.mirrorRunner = mirrorRunner;
101         zoneConfig = config.zone();
102         mirrorZoneConfig = mirrorZoneConfig(config);
103         this.accessController = accessController;
104     }
105 
106     private static Map<String, Object> mirrorZoneConfig(CentralDogmaConfig config) {
107         final MirroringServicePluginConfig mirrorConfig = mirrorConfig(config);
108         final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builderWithExpectedSize(2);
109         final boolean zonePinned = mirrorConfig != null && mirrorConfig.zonePinned();
110         builder.put("zonePinned", zonePinned);
111         final ZoneConfig zone = config.zone();
112         if (zone != null) {
113             builder.put("zone", zone);
114         }
115         return builder.build();
116     }
117 
118     /**
119      * GET /projects/{projectName}/mirrors
120      *
121      * <p>Returns the list of the mirrors in the project.
122      */
123     @RequiresProjectRole(ProjectRole.OWNER)
124     @Get("/projects/{projectName}/mirrors")
125     public CompletableFuture<List<MirrorDto>> listProjectMirrors(@Param String projectName) {
126         final CompletableFuture<List<Mirror>> future = metaRepo(projectName).mirrors(true);
127         return convertToMirrorDtos(projectName, future);
128     }
129 
130     /**
131      * GET /projects/{projectName}/repos/{repoName}/mirrors
132      *
133      * <p>Returns the list of the mirrors in the repository.
134      */
135     @RequiresRepositoryRole(RepositoryRole.ADMIN)
136     @Get("/projects/{projectName}/repos/{repoName}/mirrors")
137     public CompletableFuture<List<MirrorDto>> listRepoMirrors(@Param String projectName,
138                                                               Repository repository) {
139         final CompletableFuture<List<Mirror>> future = metaRepo(projectName).mirrors(repository.name(), true);
140         return convertToMirrorDtos(projectName, future);
141     }
142 
143     /**
144      * GET /projects/{projectName}/repos/{repoName}/mirrors/{id}
145      *
146      * <p>Returns the mirror of the ID in the project mirror list.
147      */
148     @RequiresRepositoryRole(RepositoryRole.ADMIN)
149     @Get("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
150     public CompletableFuture<MirrorDto> getMirror(@Param String projectName,
151                                                   Repository repository,
152                                                   @Param String id) {
153         return metaRepo(projectName).mirror(repository.name(), id).thenCompose(mirror -> {
154             return accessController.isAllowed(mirror.remoteRepoUri()).thenApply(allowed -> {
155                 return convertToMirrorDto(projectName, mirror, allowed);
156             });
157         });
158     }
159 
160     /**
161      * POST /projects/{projectName}/repos/{repoName}/mirrors
162      *
163      * <p>Creates a new mirror.
164      */
165     @Post("/projects/{projectName}/repos/{repoName}/mirrors")
166     @ConsumesJson
167     @StatusCode(201)
168     @RequiresRepositoryRole(RepositoryRole.ADMIN)
169     public CompletableFuture<PushResultDto> createMirror(@Param String projectName,
170                                                          Repository repository,
171                                                          MirrorRequest newMirror,
172                                                          Author author, User user) {
173         return createOrUpdate(projectName, repository.name(), newMirror, author, user, false);
174     }
175 
176     /**
177      * PUT /projects/{projectName}/repos/{repoName}/mirrors
178      *
179      * <p>Update the exising mirror.
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      * DELETE /projects/{projectName}/repos/{repoName}/mirrors/{id}
194      *
195      * <p>Delete the existing mirror.
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             // mirror exists.
206             final Command<CommitResult> 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         return metaRepo.createMirrorPushCommand(repoName, newMirror, author, zoneConfig, update).thenCompose(
219                 command -> {
220                     return executor().execute(command).thenApply(result -> {
221                         metaRepo.mirror(repoName, newMirror.id(), result.revision())
222                                 .handle((mirror, cause) -> {
223                                     if (cause != null) {
224                                         // This should not happen in normal cases.
225                                         logger.warn("Failed to get the mirror: {}", newMirror.id(), cause);
226                                         return null;
227                                     }
228                                     return notifyMirrorEvent(mirror, user, update);
229                                 });
230                         return new PushResultDto(result.revision(), command.timestamp());
231                     });
232                 });
233     }
234 
235     private Void notifyMirrorEvent(Mirror mirror, User user, boolean update) {
236         try {
237             final MirrorListener listener = MirrorSchedulingService.mirrorListener();
238             if (update) {
239                 listener.onUpdate(mirror, user, accessController);
240             } else {
241                 listener.onCreate(mirror, user, accessController);
242             }
243         } catch (Throwable ex) {
244             logger.warn("Failed to notify the mirror listener. (mirror: {})", mirror, ex);
245         }
246         return null;
247     }
248 
249     /**
250      * POST /projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run
251      *
252      * <p>Runs the mirroring task immediately.
253      */
254     // Mirroring may be a long-running task, so we need to increase the timeout.
255     @RequestTimeout(value = 5, unit = TimeUnit.MINUTES)
256     @Post("/projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run")
257     @RequiresRepositoryRole(RepositoryRole.ADMIN)
258     public CompletableFuture<MirrorResult> runMirror(@Param String projectName,
259                                                      Repository repository,
260                                                      @Param String mirrorId,
261                                                      User user) throws Exception {
262         return mirrorRunner.run(projectName, repository.name(), mirrorId, user);
263     }
264 
265     /**
266      * GET /mirror/config
267      *
268      * <p>Returns the configuration of the mirroring service.
269      */
270     @Get("/mirror/config")
271     public Map<String, Object> config() {
272         // TODO(ikhoon): Add more configurations if necessary.
273         return mirrorZoneConfig;
274     }
275 
276     private CompletableFuture<List<MirrorDto>> convertToMirrorDtos(
277             String projectName, CompletableFuture<List<Mirror>> future) {
278         return future.thenCompose(mirrors -> {
279             final ImmutableList<String> remoteUris = mirrors.stream().map(
280                     mirror -> mirror.remoteRepoUri().toString()).collect(
281                     toImmutableList());
282             return accessController.isAllowed(remoteUris).thenApply(acl -> {
283                 return mirrors.stream()
284                               .map(mirror -> convertToMirrorDto(projectName, mirror, acl))
285                               .collect(toImmutableList());
286             });
287         });
288     }
289 
290     private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, Map<String, Boolean> acl) {
291         final boolean allowed = acl.get(mirror.remoteRepoUri().toString());
292         return convertToMirrorDto(projectName, mirror, allowed);
293     }
294 
295     private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, boolean allowed) {
296         final URI remoteRepoUri = mirror.remoteRepoUri();
297         final Cron schedule = mirror.schedule();
298         final String scheduleStr = schedule != null ? schedule.asString() : null;
299         return new MirrorDto(mirror.id(),
300                              mirror.enabled(), projectName,
301                              scheduleStr,
302                              mirror.direction().name(),
303                              mirror.localRepo().name(),
304                              mirror.localPath(),
305                              remoteRepoUri.getScheme(),
306                              remoteRepoUri.getAuthority() + remoteRepoUri.getPath(),
307                              mirror.remotePath(),
308                              mirror.remoteBranch(),
309                              mirror.gitignore(),
310                              mirror.credential().name(), mirror.zone(), allowed);
311     }
312 
313     private MetaRepository metaRepo(String projectName) {
314         return projectApiManager.getProject(projectName).metaRepo();
315     }
316 }