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.MoreObjects.firstNonNull;
20 import static com.google.common.collect.ImmutableList.toImmutableList;
21 import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.checkUnremoveArgument;
22 import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.returnOrThrow;
23 import static java.util.Objects.requireNonNull;
24
25 import java.util.List;
26 import java.util.Map;
27 import java.util.concurrent.CompletableFuture;
28
29 import javax.annotation.Nullable;
30
31 import com.fasterxml.jackson.databind.JsonNode;
32 import com.google.common.collect.ImmutableList;
33 import com.google.common.collect.ImmutableMap;
34
35 import com.linecorp.armeria.common.HttpStatus;
36 import com.linecorp.armeria.common.logging.RequestOnlyLog;
37 import com.linecorp.armeria.server.ServiceRequestContext;
38 import com.linecorp.armeria.server.annotation.Consumes;
39 import com.linecorp.armeria.server.annotation.Delete;
40 import com.linecorp.armeria.server.annotation.Get;
41 import com.linecorp.armeria.server.annotation.Param;
42 import com.linecorp.armeria.server.annotation.Patch;
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.ResponseConverter;
47 import com.linecorp.armeria.server.annotation.StatusCode;
48 import com.linecorp.centraldogma.common.Author;
49 import com.linecorp.centraldogma.common.ProjectRole;
50 import com.linecorp.centraldogma.common.RepositoryRole;
51 import com.linecorp.centraldogma.common.RepositoryStatus;
52 import com.linecorp.centraldogma.common.Revision;
53 import com.linecorp.centraldogma.internal.api.v1.CreateRepositoryRequest;
54 import com.linecorp.centraldogma.internal.api.v1.RepositoryDto;
55 import com.linecorp.centraldogma.server.command.Command;
56 import com.linecorp.centraldogma.server.command.CommandExecutor;
57 import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
58 import com.linecorp.centraldogma.server.internal.api.auth.RequiresRepositoryRole;
59 import com.linecorp.centraldogma.server.internal.api.converter.CreateApiResponseConverter;
60 import com.linecorp.centraldogma.server.metadata.MetadataService;
61 import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
62 import com.linecorp.centraldogma.server.metadata.RepositoryMetadata;
63 import com.linecorp.centraldogma.server.metadata.User;
64 import com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer;
65 import com.linecorp.centraldogma.server.storage.project.Project;
66 import com.linecorp.centraldogma.server.storage.repository.Repository;
67
68 import io.micrometer.core.instrument.Tag;
69
70
71
72
73 @ProducesJson
74 public class RepositoryServiceV1 extends AbstractService {
75
76 private final MetadataService mds;
77
78 public RepositoryServiceV1(CommandExecutor executor, MetadataService mds) {
79 super(executor);
80 this.mds = requireNonNull(mds, "mds");
81 }
82
83
84
85
86
87
88 @Get("/projects/{projectName}/repos")
89 public CompletableFuture<List<RepositoryDto>> listRepositories(ServiceRequestContext ctx, Project project,
90 @Param @Nullable String status, User user) {
91 if (status != null) {
92 HttpApiUtil.checkStatusArgument(status);
93 }
94
95 if (InternalProjectInitializer.INTERNAL_PROJECT_DOGMA.equals(project.name())) {
96 if (user.isSystemAdmin()) {
97 if (status != null) {
98 return CompletableFuture.completedFuture(removedRepositories(project));
99 }
100 return CompletableFuture.completedFuture(
101 project.repos().list().values().stream()
102 .map(repository -> DtoConverter.convert(repository, RepositoryStatus.ACTIVE))
103 .collect(toImmutableList()));
104 }
105 return HttpApiUtil.throwResponse(
106 ctx, HttpStatus.FORBIDDEN,
107 "You must be a system administrator to retrieve repositories of the dogma project.");
108 }
109
110 if (status == null) {
111 final Map<String, RepositoryMetadata> repos = projectMetadata(project).repos();
112 return CompletableFuture.completedFuture(
113 project.repos().list().values().stream()
114 .filter(r -> user.isSystemAdmin() || !Project.isInternalRepo(r.name()))
115 .map(repository -> DtoConverter.convert(repository, repos))
116 .collect(toImmutableList()));
117 }
118
119 return mds.findProjectRole(project.name(), user).handle((role, throwable) -> {
120 final boolean hasOwnerRole = role == ProjectRole.OWNER;
121 if (hasOwnerRole) {
122 return removedRepositories(project);
123 }
124 return HttpApiUtil.throwResponse(
125 ctx, HttpStatus.FORBIDDEN,
126 "You must be an owner of project '%s' to retrieve removed repositories.",
127 project.name());
128 });
129 }
130
131 private static ProjectMetadata projectMetadata(Project project) {
132 assert !InternalProjectInitializer.INTERNAL_PROJECT_DOGMA.equals(project.name());
133 final ProjectMetadata metadata = project.metadata();
134 assert metadata != null;
135 return metadata;
136 }
137
138 private static RepositoryStatus repositoryStatus(Repository repository) {
139 final Map<String, RepositoryMetadata> repos = projectMetadata(repository.parent()).repos();
140 final String repoName = normalizeRepositoryName(repository);
141
142 final RepositoryMetadata metadata = repos.get(repoName);
143 if (metadata == null) {
144 return RepositoryStatus.ACTIVE;
145 } else {
146 return metadata.status();
147 }
148 }
149
150 private static String normalizeRepositoryName(Repository repository) {
151 final String repoName = repository.name();
152 if (!Project.REPO_META.equals(repoName)) {
153 return repoName;
154 }
155
156 return Project.REPO_DOGMA;
157 }
158
159 private static ImmutableList<RepositoryDto> removedRepositories(Project project) {
160 return project.repos().listRemoved().keySet().stream().map(RepositoryDto::removed)
161 .collect(toImmutableList());
162 }
163
164
165
166
167
168
169 @Post("/projects/{projectName}/repos")
170 @StatusCode(201)
171 @ResponseConverter(CreateApiResponseConverter.class)
172 @RequiresProjectRole(ProjectRole.MEMBER)
173 public CompletableFuture<RepositoryDto> createRepository(ServiceRequestContext ctx, Project project,
174 CreateRepositoryRequest request,
175 Author author) {
176 final String repoName = request.name();
177 if (Project.isInternalRepo(repoName)) {
178 return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
179 "An internal repository cannot be created.");
180 }
181 final CommandExecutor commandExecutor = executor();
182 final CompletableFuture<Revision> future =
183 RepositoryServiceUtil.createRepository(commandExecutor, mds, author, project.name(), repoName);
184 return future.handle(returnOrThrow(() -> {
185 final Repository repository = project.repos().get(repoName);
186 return DtoConverter.convert(repository, repositoryStatus(repository));
187 }));
188 }
189
190
191
192
193
194
195 @Delete("/projects/{projectName}/repos/{repoName}")
196 @RequiresRepositoryRole(RepositoryRole.ADMIN)
197 public CompletableFuture<Void> removeRepository(ServiceRequestContext ctx,
198 @Param String repoName,
199 Repository repository,
200 Author author) {
201 if (Project.isInternalRepo(repoName)) {
202 return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
203 "An internal repository cannot be removed.");
204 }
205 return RepositoryServiceUtil.removeRepository(executor(), mds, author,
206 repository.parent().name(), repoName)
207 .handle(HttpApiUtil::throwUnsafelyIfNonNull);
208 }
209
210
211
212
213
214
215 @Delete("/projects/{projectName}/repos/{repoName}/removed")
216 @RequiresProjectRole(ProjectRole.OWNER)
217 public CompletableFuture<Void> purgeRepository(@Param String repoName,
218 Project project, Author author) {
219 return execute(Command.purgeRepository(author, project.name(), repoName))
220 .thenCompose(unused -> mds.purgeRepo(author, project.name(), repoName)
221 .handle(HttpApiUtil::throwUnsafelyIfNonNull));
222 }
223
224
225
226
227
228
229
230
231 @Consumes("application/json-patch+json")
232 @Patch("/projects/{projectName}/repos/{repoName}")
233 @RequiresProjectRole(ProjectRole.OWNER)
234 public CompletableFuture<RepositoryDto> patchRepository(@Param String repoName,
235 Project project,
236 JsonNode node,
237 Author author) {
238 checkUnremoveArgument(node);
239 return execute(Command.unremoveRepository(author, project.name(), repoName))
240 .thenCompose(unused -> mds.restoreRepo(author, project.name(), repoName))
241 .handle(returnOrThrow(() -> {
242 final Repository repository = project.repos().get(repoName);
243 return DtoConverter.convert(repository, repositoryStatus(repository));
244 }));
245 }
246
247
248
249
250
251
252 @Get("/projects/{projectName}/repos/{repoName}/revision/{revision}")
253 @RequiresRepositoryRole(RepositoryRole.READ)
254 public Map<String, Integer> normalizeRevision(ServiceRequestContext ctx,
255 Repository repository, @Param String revision) {
256 final Revision normalizedRevision = repository.normalizeNow(new Revision(revision));
257 final Revision head = repository.normalizeNow(Revision.HEAD);
258 increaseCounterIfOldRevisionUsed(ctx, repository, normalizedRevision, head);
259 return ImmutableMap.of("revision", normalizedRevision.major());
260 }
261
262
263
264
265
266
267 @Get("/projects/{projectName}/repos/{repoName}")
268 @RequiresRepositoryRole(RepositoryRole.ADMIN)
269 public RepositoryDto status(Project project, Repository repository) {
270 validateDogmaProject(project);
271 return DtoConverter.convert(repository, repositoryStatus(repository));
272 }
273
274
275
276
277
278
279 @Put("/projects/{projectName}/repos/{repoName}/status")
280 @Consumes("application/json")
281 @RequiresRepositoryRole(RepositoryRole.ADMIN)
282 public CompletableFuture<RepositoryDto> updateStatus(Project project,
283 Repository repository,
284 Author author,
285 UpdateRepositoryStatusRequest statusRequest)
286 throws Exception {
287 validateDogmaProject(project);
288 final RepositoryStatus oldStatus = repositoryStatus(repository);
289 final RepositoryStatus newStatus = statusRequest.status();
290 if (oldStatus == newStatus) {
291
292 return CompletableFuture.completedFuture(DtoConverter.convert(repository, oldStatus));
293 }
294
295 return mds.updateRepositoryStatus(author, project.name(),
296 normalizeRepositoryName(repository), newStatus)
297 .thenApply(unused -> DtoConverter.convert(repository, newStatus));
298 }
299
300 static void increaseCounterIfOldRevisionUsed(ServiceRequestContext ctx, Repository repository,
301 Revision revision) {
302 final Revision normalized = repository.normalizeNow(revision);
303 final Revision head = repository.normalizeNow(Revision.HEAD);
304 increaseCounterIfOldRevisionUsed(ctx, repository, normalized, head);
305 }
306
307 public static void increaseCounterIfOldRevisionUsed(
308 ServiceRequestContext ctx, Repository repository, Revision normalized, Revision head) {
309 final String projectName = repository.parent().name();
310 final String repoName = repository.name();
311 if (normalized.major() == 1) {
312 ctx.log().whenRequestComplete().thenAccept(
313 log -> ctx.meterRegistry()
314 .counter("revisions.init", generateTags(projectName, repoName, log).build())
315 .increment());
316 }
317 if (head.major() - normalized.major() >= 5000) {
318 ctx.log().whenRequestComplete().thenAccept(
319 log -> ctx.meterRegistry()
320 .summary("revisions.old",
321 generateTags(projectName, repoName, log)
322 .add(Tag.of("init", Boolean.toString(normalized.major() == 1)))
323 .build())
324 .record(head.major() - normalized.major()));
325 }
326 }
327
328 private static ImmutableList.Builder<Tag> generateTags(
329 String projectName, String repoName, RequestOnlyLog log) {
330 final ImmutableList.Builder<Tag> builder = ImmutableList.builder();
331 return builder.add(Tag.of("project", projectName),
332 Tag.of("repo", repoName),
333 Tag.of("service", firstNonNull(log.serviceName(), "none")),
334 Tag.of("method", log.name()));
335 }
336
337 private static void validateDogmaProject(Project project) {
338 if (InternalProjectInitializer.INTERNAL_PROJECT_DOGMA.equals(project.name())) {
339 throw new IllegalArgumentException(
340 "Cannot update the status of the internal project: " + project.name());
341 }
342 }
343 }