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.ExceptionHandler;
41 import com.linecorp.armeria.server.annotation.Get;
42 import com.linecorp.armeria.server.annotation.Param;
43 import com.linecorp.armeria.server.annotation.Patch;
44 import com.linecorp.armeria.server.annotation.Post;
45 import com.linecorp.armeria.server.annotation.ProducesJson;
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.Revision;
50 import com.linecorp.centraldogma.internal.api.v1.CreateRepositoryRequest;
51 import com.linecorp.centraldogma.internal.api.v1.RepositoryDto;
52 import com.linecorp.centraldogma.server.command.Command;
53 import com.linecorp.centraldogma.server.command.CommandExecutor;
54 import com.linecorp.centraldogma.server.internal.api.auth.RequiresReadPermission;
55 import com.linecorp.centraldogma.server.internal.api.auth.RequiresRole;
56 import com.linecorp.centraldogma.server.internal.api.converter.CreateApiResponseConverter;
57 import com.linecorp.centraldogma.server.metadata.MetadataService;
58 import com.linecorp.centraldogma.server.metadata.ProjectRole;
59 import com.linecorp.centraldogma.server.metadata.User;
60 import com.linecorp.centraldogma.server.storage.project.Project;
61 import com.linecorp.centraldogma.server.storage.repository.Repository;
62
63 import io.micrometer.core.instrument.Tag;
64
65
66
67
68 @ProducesJson
69 @ExceptionHandler(HttpApiExceptionHandler.class)
70 public class RepositoryServiceV1 extends AbstractService {
71
72 private final MetadataService mds;
73
74 public RepositoryServiceV1(CommandExecutor executor, MetadataService mds) {
75 super(executor);
76 this.mds = requireNonNull(mds, "mds");
77 }
78
79
80
81
82
83
84 @Get("/projects/{projectName}/repos")
85 public CompletableFuture<List<RepositoryDto>> listRepositories(ServiceRequestContext ctx, Project project,
86 @Param @Nullable String status, User user) {
87 if (status != null) {
88 HttpApiUtil.checkStatusArgument(status);
89 }
90
91 return mds.findRole(project.name(), user).handle((role, throwable) -> {
92 final boolean hasOwnerRole = role == ProjectRole.OWNER;
93 if (status != null) {
94 if (hasOwnerRole) {
95 return project.repos().listRemoved().keySet().stream().map(RepositoryDto::new)
96 .collect(toImmutableList());
97 }
98 return HttpApiUtil.throwResponse(
99 ctx, HttpStatus.FORBIDDEN,
100 "You must be an owner of project '%s' to retrieve removed repositories.",
101 project.name());
102 }
103
104 return project.repos().list().values().stream()
105 .filter(r -> user.isAdmin() || !Project.REPO_DOGMA.equals(r.name()))
106 .filter(r -> hasOwnerRole || !Project.REPO_META.equals(r.name()))
107 .map(DtoConverter::convert)
108 .collect(toImmutableList());
109 });
110 }
111
112
113
114
115
116
117 @Post("/projects/{projectName}/repos")
118 @StatusCode(201)
119 @ResponseConverter(CreateApiResponseConverter.class)
120 @RequiresRole(roles = ProjectRole.OWNER)
121 public CompletableFuture<RepositoryDto> createRepository(ServiceRequestContext ctx, Project project,
122 CreateRepositoryRequest request,
123 Author author) {
124 if (Project.isReservedRepoName(request.name())) {
125 return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
126 "A reserved repository cannot be created.");
127 }
128 return execute(Command.createRepository(author, project.name(), request.name()))
129 .thenCompose(unused -> mds.addRepo(author, project.name(), request.name()))
130 .handle(returnOrThrow(() -> DtoConverter.convert(project.repos().get(request.name()))));
131 }
132
133
134
135
136
137
138 @Delete("/projects/{projectName}/repos/{repoName}")
139 @RequiresRole(roles = ProjectRole.OWNER)
140 public CompletableFuture<Void> removeRepository(ServiceRequestContext ctx,
141 @Param String repoName,
142 Repository repository,
143 Author author) {
144 if (Project.isReservedRepoName(repoName)) {
145 return HttpApiUtil.throwResponse(ctx, HttpStatus.FORBIDDEN,
146 "A reserved repository cannot be removed.");
147 }
148 return execute(Command.removeRepository(author, repository.parent().name(), repository.name()))
149 .thenCompose(unused -> mds.removeRepo(author, repository.parent().name(), repository.name()))
150 .handle(HttpApiUtil::throwUnsafelyIfNonNull);
151 }
152
153
154
155
156
157
158 @Delete("/projects/{projectName}/repos/{repoName}/removed")
159 @RequiresRole(roles = ProjectRole.OWNER)
160 public CompletableFuture<Void> purgeRepository(@Param String repoName,
161 Project project, Author author) {
162 return execute(Command.purgeRepository(author, project.name(), repoName))
163 .thenCompose(unused -> mds.purgeRepo(author, project.name(), repoName)
164 .handle(HttpApiUtil::throwUnsafelyIfNonNull));
165 }
166
167
168
169
170
171
172 @Consumes("application/json-patch+json")
173 @Patch("/projects/{projectName}/repos/{repoName}")
174 @RequiresRole(roles = ProjectRole.OWNER)
175 public CompletableFuture<RepositoryDto> patchRepository(@Param String repoName,
176 Project project,
177 JsonNode node,
178 Author author) {
179 checkUnremoveArgument(node);
180 return execute(Command.unremoveRepository(author, project.name(), repoName))
181 .thenCompose(unused -> mds.restoreRepo(author, project.name(), repoName))
182 .handle(returnOrThrow(() -> DtoConverter.convert(project.repos().get(repoName))));
183 }
184
185
186
187
188
189
190 @Get("/projects/{projectName}/repos/{repoName}/revision/{revision}")
191 @RequiresReadPermission
192 public Map<String, Integer> normalizeRevision(ServiceRequestContext ctx,
193 Repository repository, @Param String revision) {
194 final Revision normalizedRevision = repository.normalizeNow(new Revision(revision));
195 final Revision head = repository.normalizeNow(Revision.HEAD);
196 increaseCounterIfOldRevisionUsed(ctx, repository, normalizedRevision, head);
197 return ImmutableMap.of("revision", normalizedRevision.major());
198 }
199
200 static void increaseCounterIfOldRevisionUsed(ServiceRequestContext ctx, Repository repository,
201 Revision revision) {
202 final Revision normalized = repository.normalizeNow(revision);
203 final Revision head = repository.normalizeNow(Revision.HEAD);
204 increaseCounterIfOldRevisionUsed(ctx, repository, normalized, head);
205 }
206
207 public static void increaseCounterIfOldRevisionUsed(
208 ServiceRequestContext ctx, Repository repository, Revision normalized, Revision head) {
209 final String projectName = repository.parent().name();
210 final String repoName = repository.name();
211 if (normalized.major() == 1) {
212 ctx.log().whenRequestComplete().thenAccept(
213 log -> ctx.meterRegistry()
214 .counter("revisions.init", generateTags(projectName, repoName, log).build())
215 .increment());
216 }
217 if (head.major() - normalized.major() >= 5000) {
218 ctx.log().whenRequestComplete().thenAccept(
219 log -> ctx.meterRegistry()
220 .summary("revisions.old",
221 generateTags(projectName, repoName, log)
222 .add(Tag.of("init", Boolean.toString(normalized.major() == 1)))
223 .build())
224 .record(head.major() - normalized.major()));
225 }
226 }
227
228 private static ImmutableList.Builder<Tag> generateTags(
229 String projectName, String repoName, RequestOnlyLog log) {
230 final ImmutableList.Builder<Tag> builder = ImmutableList.builder();
231 return builder.add(Tag.of("project", projectName),
232 Tag.of("repo", repoName),
233 Tag.of("service", firstNonNull(log.serviceName(), "none")),
234 Tag.of("method", log.name()));
235 }
236 }