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  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 javax.annotation.Nullable;
32  
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  
36  import com.cronutils.model.Cron;
37  import com.google.common.collect.ImmutableList;
38  import com.google.common.collect.ImmutableMap;
39  
40  import com.linecorp.armeria.server.annotation.ConsumesJson;
41  import com.linecorp.armeria.server.annotation.Delete;
42  import com.linecorp.armeria.server.annotation.Get;
43  import com.linecorp.armeria.server.annotation.Param;
44  import com.linecorp.armeria.server.annotation.Post;
45  import com.linecorp.armeria.server.annotation.ProducesJson;
46  import com.linecorp.armeria.server.annotation.Put;
47  import com.linecorp.armeria.server.annotation.StatusCode;
48  import com.linecorp.armeria.server.annotation.decorator.RequestTimeout;
49  import com.linecorp.centraldogma.common.Author;
50  import com.linecorp.centraldogma.common.Change;
51  import com.linecorp.centraldogma.common.Markup;
52  import com.linecorp.centraldogma.common.ProjectRole;
53  import com.linecorp.centraldogma.common.RepositoryRole;
54  import com.linecorp.centraldogma.common.Revision;
55  import com.linecorp.centraldogma.internal.api.v1.MirrorDto;
56  import com.linecorp.centraldogma.internal.api.v1.MirrorRequest;
57  import com.linecorp.centraldogma.internal.api.v1.PushResultDto;
58  import com.linecorp.centraldogma.server.CentralDogmaConfig;
59  import com.linecorp.centraldogma.server.ZoneConfig;
60  import com.linecorp.centraldogma.server.command.Command;
61  import com.linecorp.centraldogma.server.command.CommandExecutor;
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         validateMirrorId(newMirror.id());
174         return createOrUpdate(projectName, repository.name(), newMirror, author, user, false);
175     }
176 
177     /**
178      * PUT /projects/{projectName}/repos/{repoName}/mirrors
179      *
180      * <p>Update the exising mirror.
181      */
182     @ConsumesJson
183     @Put("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
184     @RequiresRepositoryRole(RepositoryRole.ADMIN)
185     public CompletableFuture<PushResultDto> updateMirror(@Param String projectName,
186                                                          Repository repository,
187                                                          MirrorRequest mirror,
188                                                          @Param String id, Author author, User user) {
189         checkArgument(id.equals(mirror.id()), "The mirror ID (%s) can't be updated", id);
190         return createOrUpdate(projectName, repository.name(), mirror, author, user, true);
191     }
192 
193     /**
194      * DELETE /projects/{projectName}/repos/{repoName}/mirrors/{id}
195      *
196      * <p>Delete the existing mirror.
197      */
198     @Delete("/projects/{projectName}/repos/{repoName}/mirrors/{id}")
199     @RequiresRepositoryRole(RepositoryRole.ADMIN)
200     public CompletableFuture<Void> deleteMirror(@Param String projectName,
201                                                 Repository repository,
202                                                 @Param String id, Author author) {
203         final MetaRepository metaRepository = metaRepo(projectName);
204         final String repoName = repository.name();
205         return metaRepository.mirror(repoName, id).thenCompose(mirror -> {
206             // mirror exists.
207             final Command<Revision> command =
208                     Command.push(author, projectName, metaRepository.name(),
209                                  Revision.HEAD, "Delete mirror: " + id + " in " + repoName, "",
210                                  Markup.PLAINTEXT, Change.ofRemoval(mirrorFile(repoName, id)));
211             return executor().execute(command).thenApply(result -> null);
212         });
213     }
214 
215     private CompletableFuture<PushResultDto> createOrUpdate(
216             String projectName, String repoName, MirrorRequest newMirror,
217             Author author, User user, boolean update) {
218         final MetaRepository metaRepo = metaRepo(projectName);
219 
220         return metaRepo.createMirrorPushCommand(repoName, newMirror, author, zoneConfig, update).thenCompose(
221                 command -> {
222                     return executor().execute(command).thenApply(revision -> {
223                         metaRepo.mirror(repoName, newMirror.id(), revision)
224                                 .handle((mirror, cause) -> {
225                                     if (cause != null) {
226                                         // This should not happen in normal cases.
227                                         logger.warn("Failed to get the mirror: {}", newMirror.id(), cause);
228                                         return null;
229                                     }
230                                     return notifyMirrorEvent(mirror, user, update);
231                                 });
232                         return new PushResultDto(revision, command.timestamp());
233                     });
234                 });
235     }
236 
237     private Void notifyMirrorEvent(Mirror mirror, User user, boolean update) {
238         try {
239             final MirrorListener listener = MirrorSchedulingService.mirrorListener();
240             if (update) {
241                 listener.onUpdate(mirror, user, accessController);
242             } else {
243                 listener.onCreate(mirror, user, accessController);
244             }
245         } catch (Throwable ex) {
246             logger.warn("Failed to notify the mirror listener. (mirror: {})", mirror, ex);
247         }
248         return null;
249     }
250 
251     /**
252      * POST /projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run
253      *
254      * <p>Runs the mirroring task immediately.
255      */
256     // Mirroring may be a long-running task, so we need to increase the timeout.
257     @RequestTimeout(value = 5, unit = TimeUnit.MINUTES)
258     @Post("/projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run")
259     @RequiresRepositoryRole(RepositoryRole.ADMIN)
260     public CompletableFuture<MirrorResult> runMirror(@Param String projectName,
261                                                      Repository repository,
262                                                      @Param String mirrorId,
263                                                      User user) throws Exception {
264         return mirrorRunner.run(projectName, repository.name(), mirrorId, user);
265     }
266 
267     /**
268      * GET /mirror/config
269      *
270      * <p>Returns the configuration of the mirroring service.
271      */
272     @Get("/mirror/config")
273     public Map<String, Object> config() {
274         // TODO(ikhoon): Add more configurations if necessary.
275         return mirrorZoneConfig;
276     }
277 
278     private CompletableFuture<List<MirrorDto>> convertToMirrorDtos(
279             String projectName, CompletableFuture<List<Mirror>> future) {
280         return future.thenCompose(mirrors -> {
281             final ImmutableList<String> remoteUris = mirrors.stream().map(
282                     mirror -> mirror.remoteRepoUri().toString()).collect(
283                     toImmutableList());
284             return accessController.isAllowed(remoteUris).thenApply(acl -> {
285                 return mirrors.stream()
286                               .map(mirror -> convertToMirrorDto(projectName, mirror, acl))
287                               .collect(toImmutableList());
288             });
289         });
290     }
291 
292     private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, Map<String, Boolean> acl) {
293         final boolean allowed = acl.get(mirror.remoteRepoUri().toString());
294         return convertToMirrorDto(projectName, mirror, allowed);
295     }
296 
297     private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, boolean allowed) {
298         final URI remoteRepoUri = mirror.remoteRepoUri();
299         final Cron schedule = mirror.schedule();
300         final String scheduleStr = schedule != null ? schedule.asString() : null;
301         return new MirrorDto(mirror.id(),
302                              mirror.enabled(), projectName,
303                              scheduleStr,
304                              mirror.direction().name(),
305                              mirror.localRepo().name(),
306                              mirror.localPath(),
307                              remoteRepoUri.getScheme(),
308                              remoteRepoUri.getAuthority() + remoteRepoUri.getPath(),
309                              mirror.remotePath(),
310                              mirror.remoteBranch(),
311                              mirror.gitignore(),
312                              mirror.credential().name(), mirror.zone(), allowed);
313     }
314 
315     private MetaRepository metaRepo(String projectName) {
316         return projectApiManager.getProject(projectName).metaRepo();
317     }
318 }