diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequest.java b/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequest.java index ea3ccc3952..1e858b3320 100644 --- a/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequest.java +++ b/common/src/main/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequest.java @@ -21,6 +21,8 @@ import static java.util.Objects.requireNonNull; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -34,6 +36,15 @@ @JsonInclude(Include.NON_NULL) public class MirrorRequest { + // TODO(minwoox): remove ._ from the ID which violates Google AIP. + public static final Pattern PROJECT_CREDENTIAL_ID_PATTERN = + Pattern.compile("^projects/([^/]+)/credentials/([a-z](?:[a-z0-9-_.]{0,61}[a-z0-9])?)$"); + + // TODO(minwoox): remove ._ from the ID. + public static final Pattern REPO_CREDENTIAL_ID_PATTERN = + Pattern.compile( + "^projects/([^/]+)/repos/([^/]+)/credentials/([a-z](?:[a-z0-9-_.]{0,61}[a-z0-9])?)$"); + private final String id; private final boolean enabled; private final String projectName; @@ -79,10 +90,56 @@ public MirrorRequest(@JsonProperty("id") String id, this.remotePath = requireNonNull(remotePath, "remotePath"); this.remoteBranch = requireNonNull(remoteBranch, "remoteBranch"); this.gitignore = gitignore; - this.credentialId = requireNonNull(credentialId, "credentialId"); + this.credentialId = validateCredentialId(projectName, localRepo, credentialId); this.zone = zone; } + private static String validateCredentialId(String projectName, String localRepo, + String credentialId) { + requireNonNull(credentialId, "credentialId"); + if (credentialId.isEmpty()) { + // Allow an empty credential ID for Credential.FALLBACK. + return ""; + } + + Matcher matcher = PROJECT_CREDENTIAL_ID_PATTERN.matcher(credentialId); + if (matcher.matches()) { + checkProjectName(projectName, matcher); + return credentialId; + } + + matcher = REPO_CREDENTIAL_ID_PATTERN.matcher(credentialId); + if (!matcher.matches()) { + throw new IllegalArgumentException("invalid credentialId: " + credentialId + " (expected: " + + PROJECT_CREDENTIAL_ID_PATTERN.pattern() + " or " + + REPO_CREDENTIAL_ID_PATTERN.pattern() + ')'); + } + checkProjectName(projectName, matcher); + final String repoNameGroup = matcher.group(2); + if (!localRepo.equals(repoNameGroup)) { + throw new IllegalArgumentException("localRepo and credentialId do not match: " + + localRepo + " vs " + repoNameGroup); + } + + return credentialId; + } + + private static void checkProjectName(String projectName, Matcher matcher) { + final String projectNameGroup = matcher.group(1); + if (!projectName.equals(projectNameGroup)) { + throw new IllegalArgumentException("projectName and credentialId do not match: " + + projectName + " vs " + projectNameGroup); + } + } + + public static String projectMirrorCredentialId(String projectName, String credentialId) { + return "projects/" + projectName + "/credentials/" + credentialId; + } + + public static String repoMirrorCredentialId(String projectName, String repoName, String credentialId) { + return "projects/" + projectName + "/repos/" + repoName + "/credentials/" + credentialId; + } + @JsonProperty("id") public String id() { return id; @@ -164,21 +221,21 @@ public boolean equals(Object o) { if (!(o instanceof MirrorRequest)) { return false; } - final MirrorRequest mirrorDto = (MirrorRequest) o; - return id.equals(mirrorDto.id) && - enabled == mirrorDto.enabled && - projectName.equals(mirrorDto.projectName) && - Objects.equals(schedule, mirrorDto.schedule) && - direction.equals(mirrorDto.direction) && - localRepo.equals(mirrorDto.localRepo) && - localPath.equals(mirrorDto.localPath) && - remoteScheme.equals(mirrorDto.remoteScheme) && - remoteUrl.equals(mirrorDto.remoteUrl) && - remotePath.equals(mirrorDto.remotePath) && - remoteBranch.equals(mirrorDto.remoteBranch) && - Objects.equals(gitignore, mirrorDto.gitignore) && - credentialId.equals(mirrorDto.credentialId) && - Objects.equals(zone, mirrorDto.zone); + final MirrorRequest mirrorRequest = (MirrorRequest) o; + return id.equals(mirrorRequest.id) && + enabled == mirrorRequest.enabled && + projectName.equals(mirrorRequest.projectName) && + Objects.equals(schedule, mirrorRequest.schedule) && + direction.equals(mirrorRequest.direction) && + localRepo.equals(mirrorRequest.localRepo) && + localPath.equals(mirrorRequest.localPath) && + remoteScheme.equals(mirrorRequest.remoteScheme) && + remoteUrl.equals(mirrorRequest.remoteUrl) && + remotePath.equals(mirrorRequest.remotePath) && + remoteBranch.equals(mirrorRequest.remoteBranch) && + Objects.equals(gitignore, mirrorRequest.gitignore) && + credentialId.equals(mirrorRequest.credentialId) && + Objects.equals(zone, mirrorRequest.zone); } @Override diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequestTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequestTest.java new file mode 100644 index 0000000000..3be03353c3 --- /dev/null +++ b/common/src/test/java/com/linecorp/centraldogma/internal/api/v1/MirrorRequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + */ +package com.linecorp.centraldogma.internal.api.v1; + +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.repoMirrorCredentialId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class MirrorRequestTest { + + @Test + void mirrorRequest() { + assertThatThrownBy(() -> newMirror("some-id")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("invalid credentialId: some-id (expected: "); + + assertThatThrownBy(() -> newMirror("projects/bar/repos/bar/credentials/credential-id")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("projectName and credentialId do not match: "); + + assertThatThrownBy(() -> newMirror("projects/foo/repos/foo/credentials/credential-id")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("localRepo and credentialId do not match: "); + + String credentialId = repoMirrorCredentialId("foo", "bar", "credential-id"); + assertThat(newMirror(credentialId).credentialId()).isEqualTo(credentialId); + + assertThatThrownBy(() -> newMirror("projects/bar/credentials/credential-id")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("projectName and credentialId do not match: "); + + credentialId = projectMirrorCredentialId("foo", "credential-id"); + assertThat(newMirror(credentialId).credentialId()).isEqualTo(credentialId); + } + + private static MirrorRequest newMirror(String credentialId) { + return new MirrorRequest("mirror-id", + true, + "foo", + "0/1 * * * * ?", + "REMOTE_TO_LOCAL", + "bar", + "/", + "git+ssh", + "github.com/line/centraldogma-authtest.git", + "/", + "main", + null, + credentialId, + null); + } +} diff --git a/it/mirror-listener/src/test/java/com/linecorp/centraldogma/it/mirror/listener/CustomMirrorListenerTest.java b/it/mirror-listener/src/test/java/com/linecorp/centraldogma/it/mirror/listener/CustomMirrorListenerTest.java index d5dd6cc8f3..d0832c93d0 100644 --- a/it/mirror-listener/src/test/java/com/linecorp/centraldogma/it/mirror/listener/CustomMirrorListenerTest.java +++ b/it/mirror-listener/src/test/java/com/linecorp/centraldogma/it/mirror/listener/CustomMirrorListenerTest.java @@ -97,7 +97,7 @@ void shouldNotifyMirrorEvents() { final Mirror mirror = new AbstractMirror("my-mirror-1", true, EVERY_SECOND, MirrorDirection.REMOTE_TO_LOCAL, - Credential.FALLBACK, r, "/", + Credential.FALLBACK, "", r, "/", URI.create("unused://uri"), "/", "", null, null) { @Override protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes, diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ForceRefUpdateTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ForceRefUpdateTest.java index 258b11c5e0..c8a51939df 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ForceRefUpdateTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ForceRefUpdateTest.java @@ -16,6 +16,7 @@ package com.linecorp.centraldogma.it.mirror.git; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static com.linecorp.centraldogma.it.mirror.git.GitTestUtil.getFileContent; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -225,7 +226,8 @@ private void pushMirror(String gitUri, MirrorDirection mirrorDirection) { " \"localPath\": \"/\"," + " \"remoteUri\": \"" + gitUri + "\"," + " \"schedule\": \"0 0 0 1 1 ? 2099\"," + - " \"credentialId\": \"public-key-id\"" + + " \"credentialId\": \"" + + projectMirrorCredentialId(projName, "public-key-id") + '"' + '}')) .push().join(); } diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorIntegrationTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorIntegrationTest.java index c6f231f4e2..b518f864e9 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorIntegrationTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/GitMirrorIntegrationTest.java @@ -17,6 +17,7 @@ package com.linecorp.centraldogma.it.mirror.git; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_COMMIT_SECTION; @@ -510,7 +511,8 @@ private void pushMirrorSettings(String localRepo, @Nullable String localPath, @N " \"localPath\": \"" + localPath0 + "\"," + " \"remoteUri\": \"" + remoteUri + "\"," + " \"schedule\": \"0 0 0 1 1 ? 2099\"," + - " \"credentialId\": \"none\"," + + " \"credentialId\": \"" + + projectMirrorCredentialId(projName, "none") + "\"," + " \"gitignore\": " + firstNonNull(gitignore, "\"\"") + '}')) .push().join(); diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/LocalToRemoteGitMirrorTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/LocalToRemoteGitMirrorTest.java index ca3dba194f..96042404e7 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/LocalToRemoteGitMirrorTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/LocalToRemoteGitMirrorTest.java @@ -17,6 +17,7 @@ package com.linecorp.centraldogma.it.mirror.git; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static com.linecorp.centraldogma.it.mirror.git.GitMirrorIntegrationTest.addToGitIndex; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -390,7 +391,8 @@ private void pushMirrorSettings(String localRepo, @Nullable String localPath, @N " \"remoteUri\": \"" + remoteUri + "\"," + " \"schedule\": \"0 0 0 1 1 ? 2099\"," + " \"gitignore\": " + firstNonNull(gitignore, "\"\"") + ',' + - " \"credentialId\": \"none\"" + + " \"credentialId\": \"" + + projectMirrorCredentialId(projName, "none") + '"' + '}')) .push().join(); } diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorAccessControlTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorAccessControlTest.java index cfdc6ce3dc..bbf94a23be 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorAccessControlTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorAccessControlTest.java @@ -16,6 +16,7 @@ package com.linecorp.centraldogma.it.mirror.git; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.repoMirrorCredentialId; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.PRIVATE_KEY_FILE; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.TEST_MIRROR_ID; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.getCredential; @@ -167,20 +168,7 @@ void testMirrorCreationEvent() throws Exception { .isOne(); }); - final MirrorRequest updating = new MirrorRequest(TEST_MIRROR_ID, - true, - TEST_PROJ, - "0/2 * * * * ?", - "REMOTE_TO_LOCAL", - TEST_REPO, - "/", - "git+ssh", - "github.com/line/centraldogma-authtest.git", - "/", - "main", - null, - PRIVATE_KEY_FILE, - null); + final MirrorRequest updating = newMirror("/foo/"); final ResponseEntity response = client.prepare() @@ -211,7 +199,7 @@ private void createMirror() throws Exception { .execute(); assertThat(response.status()).isEqualTo(HttpStatus.CREATED); - final MirrorRequest newMirror = newMirror(); + final MirrorRequest newMirror = newMirror("/"); response = client.prepare() .post("/api/v1/projects/{proj}/repos/{repo}/mirrors") .pathParam("proj", TEST_PROJ) @@ -222,20 +210,20 @@ private void createMirror() throws Exception { assertThat(response.status()).isEqualTo(HttpStatus.CREATED); } - private static MirrorRequest newMirror() { + private static MirrorRequest newMirror(String localPath) { return new MirrorRequest(TEST_MIRROR_ID, true, TEST_PROJ, "0/1 * * * * ?", "REMOTE_TO_LOCAL", TEST_REPO, - "/", + localPath, "git+ssh", "github.com/line/centraldogma-authtest.git", "/", "main", null, - PRIVATE_KEY_FILE, + repoMirrorCredentialId(TEST_PROJ, TEST_REPO, PRIVATE_KEY_FILE), null); } } diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorRunnerTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorRunnerTest.java index 49e4c4e5f7..aa1ebeb9b7 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorRunnerTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/MirrorRunnerTest.java @@ -16,6 +16,8 @@ package com.linecorp.centraldogma.it.mirror.git; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.repoMirrorCredentialId; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.PASSWORD; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.USERNAME; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.getAccessToken; @@ -42,6 +44,7 @@ import com.linecorp.centraldogma.internal.api.v1.PushResultDto; import com.linecorp.centraldogma.server.CentralDogmaBuilder; import com.linecorp.centraldogma.server.internal.api.sysadmin.MirrorAccessControlRequest; +import com.linecorp.centraldogma.server.internal.credential.NoneCredential; import com.linecorp.centraldogma.server.internal.credential.PublicKeyCredential; import com.linecorp.centraldogma.server.internal.mirror.MirrorAccessControl; import com.linecorp.centraldogma.server.mirror.MirrorResult; @@ -99,17 +102,30 @@ void setUp() throws Exception { @Test void triggerMirroring() throws Exception { - final PublicKeyCredential credential = getCredential(); + // Put an invalid credential to project with ID PRIVATE_KEY_FILE. + final NoneCredential noneCredential = new NoneCredential(PRIVATE_KEY_FILE, true); ResponseEntity response = systemAdminClient.prepare() .post("/api/v1/projects/{proj}/credentials") .pathParam("proj", FOO_PROJ) - .contentJson(credential) + .contentJson(noneCredential) .asJson(PushResultDto.class) .execute(); assertThat(response.status()).isEqualTo(HttpStatus.CREATED); - final MirrorRequest newMirror = newMirror(); + // Put valid credential to repository with ID PRIVATE_KEY_FILE. + // This credential will be used for mirroring because it has higher priority than project credential. + final PublicKeyCredential credential = getCredential(); + response = systemAdminClient.prepare() + .post("/api/v1/projects/{proj}/repos/{repo}/credentials") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", BAR_REPO) + .contentJson(credential) + .asJson(PushResultDto.class) + .execute(); + assertThat(response.status()).isEqualTo(HttpStatus.CREATED); + + final MirrorRequest newMirror = newMirror(repoMirrorCredentialId(FOO_PROJ, BAR_REPO, PRIVATE_KEY_FILE)); response = systemAdminClient.prepare() .post("/api/v1/projects/{proj}/repos/{repo}/mirrors") .pathParam("proj", FOO_PROJ) @@ -177,7 +193,7 @@ void shouldControlGitMirrorAccess() throws Exception { .execute(); assertThat(response.status()).isEqualTo(HttpStatus.CREATED); - final MirrorRequest newMirror = newMirror(); + final MirrorRequest newMirror = newMirror(projectMirrorCredentialId(FOO_PROJ, PRIVATE_KEY_FILE)); response = systemAdminClient.prepare() .post("/api/v1/projects/{proj}/repos/{repo}/mirrors") .pathParam("proj", FOO_PROJ) @@ -217,7 +233,7 @@ void shouldControlGitMirrorAccess() throws Exception { assertThat(mirrorResponse.status()).isEqualTo(HttpStatus.OK); } - private static MirrorRequest newMirror() { + private static MirrorRequest newMirror(String credentialId) { return new MirrorRequest(TEST_MIRROR_ID, true, FOO_PROJ, @@ -230,7 +246,7 @@ private static MirrorRequest newMirror() { "/", "main", null, - PRIVATE_KEY_FILE, + credentialId, null); } diff --git a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ZoneAwareMirrorTest.java b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ZoneAwareMirrorTest.java index d97186d68d..613d0b9471 100644 --- a/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ZoneAwareMirrorTest.java +++ b/it/mirror/src/test/java/com/linecorp/centraldogma/it/mirror/git/ZoneAwareMirrorTest.java @@ -16,6 +16,8 @@ package com.linecorp.centraldogma.it.mirror.git; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.repoMirrorCredentialId; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.BAR_REPO; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.FOO_PROJ; import static com.linecorp.centraldogma.it.mirror.git.MirrorRunnerTest.PRIVATE_KEY_FILE; @@ -178,7 +180,7 @@ void shouldWarnUnknownZoneForScheduledJob() throws Exception { "/", URI.create("git+ssh://github.com/line/centraldogma-authtest.git/#main"), null, - "foo", + repoMirrorCredentialId("foo", "bar-unknown-zone", "credential-id"), unknownZone); final Change change = Change.ofJsonUpsert( "/repos/bar-unknown-zone/mirrors/" + mirrorId + ".json", @@ -244,7 +246,7 @@ private static MirrorRequest newMirror(@Nullable String zone) { "/", "main", null, - PRIVATE_KEY_FILE, + projectMirrorCredentialId(FOO_PROJ, PRIVATE_KEY_FILE), zone); } } diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java index bff561b3d5..122f2c1a8b 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractGitMirror.java @@ -123,11 +123,11 @@ abstract class AbstractGitMirror extends AbstractMirror { private IgnoreNode ignoreNode; AbstractGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, + Credential credential, String mirrorCredentialId, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore, @Nullable String zone) { - super(id, enabled, schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, - remoteBranch, gitignore, zone); + super(id, enabled, schedule, direction, credential, mirrorCredentialId, localRepo, localPath, + remoteRepoUri, remotePath, remoteBranch, gitignore, zone); if (gitignore != null) { ignoreNode = new IgnoreNode(); diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java index e92a893453..ccc7cab281 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/DefaultGitMirror.java @@ -45,11 +45,11 @@ final class DefaultGitMirror extends AbstractGitMirror { private static final Consumer> NOOP_CONFIGURATOR = command -> {}; DefaultGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, + Credential credential, String mirrorCredentialId, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore, @Nullable String zone) { - super(id, enabled, schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, - remoteBranch, gitignore, zone); + super(id, enabled, schedule, direction, credential, mirrorCredentialId, localRepo, localPath, + remoteRepoUri, remotePath, remoteBranch, gitignore, zone); } @Override diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirrorProvider.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirrorProvider.java index 51711b9fca..a334bb9eda 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirrorProvider.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirrorProvider.java @@ -47,6 +47,7 @@ public Mirror newMirror(MirrorContext context) { final RepositoryUri repositoryUri = RepositoryUri.parse(remoteUri, "git"); return new SshGitMirror(context.id(), context.enabled(), context.schedule(), context.direction(), context.credential(), + context.mirrorCredentialId(), context.localRepo(), context.localPath(), repositoryUri.uri(), repositoryUri.path(), repositoryUri.branch(), context.gitignore(), context.zone()); @@ -58,6 +59,7 @@ public Mirror newMirror(MirrorContext context) { final RepositoryUri repositoryUri = RepositoryUri.parse(remoteUri, "git"); return new DefaultGitMirror(context.id(), context.enabled(), context.schedule(), context.direction(), context.credential(), + context.mirrorCredentialId(), context.localRepo(), context.localPath(), repositoryUri.uri(), repositoryUri.path(), repositoryUri.branch(), context.gitignore(), context.zone()); diff --git a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java index 9421e304ca..6bfef459a7 100644 --- a/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java +++ b/server-mirror-git/src/main/java/com/linecorp/centraldogma/server/internal/mirror/SshGitMirror.java @@ -83,11 +83,11 @@ final class SshGitMirror extends AbstractGitMirror { private static final BouncyCastleRandom bounceCastleRandom = new BouncyCastleRandom(); SshGitMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, + Credential credential, String mirrorCredentialId, Repository localRepo, String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore, @Nullable String zone) { - super(id, enabled, schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, - remoteBranch, gitignore, zone); + super(id, enabled, schedule, direction, credential, mirrorCredentialId, localRepo, localPath, + remoteRepoUri, remotePath, remoteBranch, gitignore, zone); } @Override diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java index 8e24c420a0..af5baabb7c 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/DefaultMetaRepositoryWithMirrorTest.java @@ -17,6 +17,7 @@ package com.linecorp.centraldogma.server.internal.mirror; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static com.linecorp.centraldogma.server.internal.storage.repository.MirrorConfig.DEFAULT_SCHEDULE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -152,7 +153,7 @@ void testMirror(boolean useRawApi) { " \"localRepo\": \"foo\"," + " \"localPath\": \"/mirrors/foo\"," + " \"remoteUri\": \"git+ssh://foo.com/foo.git\"," + - " \"credentialId\": \"alice\"" + + " \"credentialId\": \"" + projectMirrorCredentialId(project.name(), "alice") + '"' + '}'), Change.ofJsonUpsert( "/repos/repo/mirrors/bar.json", @@ -163,17 +164,18 @@ void testMirror(boolean useRawApi) { " \"direction\": \"REMOTE_TO_LOCAL\"," + " \"localRepo\": \"bar\"," + " \"remoteUri\": \"git+ssh://bar.com/bar.git/some-path#develop\"," + - " \"credentialId\": \"bob\"" + + " \"credentialId\": \"" + projectMirrorCredentialId(project.name(), "bob") + '"' + '}')); metaRepo.commit(Revision.HEAD, 0L, Author.SYSTEM, "", mirrors).join(); metaRepo.commit(Revision.HEAD, 0L, Author.SYSTEM, "", UPSERT_RAW_CREDENTIALS).join(); } else { final List mirrors = ImmutableList.of( new MirrorRequest("foo", true, project.name(), DEFAULT_SCHEDULE, "LOCAL_TO_REMOTE", "foo", - "/mirrors/foo", "git+ssh", "foo.com/foo.git", "", "", null, "alice", null), + "/mirrors/foo", "git+ssh", "foo.com/foo.git", "", "", null, + projectMirrorCredentialId(project.name(), "alice"), null), new MirrorRequest("bar", true, project.name(), "0 */10 * * * ?", "REMOTE_TO_LOCAL", "bar", - "", "git+ssh", "bar.com/bar.git", "/some-path", "develop", null, "bob", - null)); + "", "git+ssh", "bar.com/bar.git", "/some-path", "develop", null, + projectMirrorCredentialId(project.name(), "bob"), null)); for (Credential credential : CREDENTIALS) { final Command command = metaRepo.createCredentialPushCommand(credential, Author.SYSTEM, false).join(); @@ -248,7 +250,8 @@ void testMirrorWithCredentialId() { " \"direction\": \"LOCAL_TO_REMOTE\"," + " \"localRepo\": \"qux\"," + " \"remoteUri\": \"git+ssh://qux.net/qux.git\"," + - " \"credentialId\": \"alice\"" + + " \"credentialId\": \"" + + projectMirrorCredentialId(project.name(), "alice") + '"' + '}')) .addAll(UPSERT_RAW_CREDENTIALS) .build(); diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingServiceTest.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingServiceTest.java index dffc12c83b..6cd2145a0a 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingServiceTest.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorSchedulingServiceTest.java @@ -77,7 +77,7 @@ void mirroringTaskShouldNeverBeRejected() { final Mirror mirror = new AbstractMirror("my-mirror-1", true, EVERY_SECOND, MirrorDirection.REMOTE_TO_LOCAL, - Credential.FALLBACK, r, "/", + Credential.FALLBACK, "", r, "/", URI.create("unused://uri"), "/", "", null, null) { @Override protected MirrorResult mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes, diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringAndCredentialServiceV1Test.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringAndCredentialServiceV1Test.java index 03b8f15f8b..d0380404aa 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringAndCredentialServiceV1Test.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringAndCredentialServiceV1Test.java @@ -16,6 +16,9 @@ package com.linecorp.centraldogma.server.internal.mirror; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.repoMirrorCredentialId; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.PASSWORD; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.PASSWORD2; import static com.linecorp.centraldogma.testing.internal.auth.TestAuthMessageUtil.USERNAME; @@ -25,14 +28,17 @@ import java.util.List; import java.util.Map; +import java.util.Set; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.linecorp.armeria.client.BlockingWebClient; import com.linecorp.armeria.client.WebClient; @@ -59,6 +65,7 @@ class MirroringAndCredentialServiceV1Test { private static final String FOO_PROJ = "foo-proj"; private static final String BAR_REPO = "bar-repo"; + private static final String BAR2_REPO = "bar2-repo"; @RegisterExtension static final CentralDogmaExtension dogma = new CentralDogmaExtension() { @@ -81,14 +88,15 @@ protected void configureClient(ArmeriaCentralDogmaBuilder builder) { protected void scaffold(CentralDogma client) { client.createProject(FOO_PROJ).join(); client.createRepository(FOO_PROJ, BAR_REPO).join(); + client.createRepository(FOO_PROJ, BAR2_REPO).join(); } }; - private BlockingWebClient systemAdminClient; - private BlockingWebClient userClient; + private static BlockingWebClient systemAdminClient; + private static BlockingWebClient userClient; - @BeforeEach - void setUp() throws JsonProcessingException { + @BeforeAll + static void setUp() throws JsonProcessingException { final String systemAdminToken = getAccessToken(dogma.httpClient(), USERNAME, PASSWORD); systemAdminClient = WebClient.builder(dogma.httpClient().uri()) .auth(AuthToken.ofOAuth2(systemAdminToken)) @@ -100,21 +108,94 @@ void setUp() throws JsonProcessingException { .auth(AuthToken.ofOAuth2(userToken)) .build() .blocking(); + setUpRole(); } @Test - void cruTest() { - setUpRole(); + void crudTest() { createAndReadCredential(); updateCredential(); - createAndReadMirror(); + createAndReadMirror(BAR_REPO); + createAndReadMirror(BAR2_REPO); + ResponseEntity> response = + userClient.prepare() + .get("/api/v1/projects/{proj}/repos/{repo}/mirrors") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", BAR_REPO) + .asJson(new TypeReference>() {}) + .execute(); + assertThat(response.content().size()).isEqualTo(4); // mirror-0, mirror-1, mirror-2, mirror-with-port-3 + assertThat(response.content().stream().map(MirrorRequest::localRepo) + .distinct() + .collect(toImmutableList())).containsExactly(BAR_REPO); + response = userClient.prepare() + .get("/api/v1/projects/{proj}/mirrors") + .pathParam("proj", FOO_PROJ) + .asJson(new TypeReference>() {}) + .execute(); + assertThat(response.content().size()).isEqualTo(8); + assertThat(response.content().stream().map(MirrorRequest::localRepo) + .distinct() + .collect(toImmutableList())).containsExactlyInAnyOrder(BAR_REPO, BAR2_REPO); updateMirror(); rejectInvalidRepositoryUri(); - deleteMirror(); + deleteMirror(BAR_REPO); + deleteMirror(BAR2_REPO); deleteCredential(); } - private void rejectInvalidRepositoryUri() { + @Test + void repoAndProjectCredentialsUsed() { + // Make sure there are no project and repo credentials. + AggregatedHttpResponse response = + systemAdminClient.prepare() + .get("/api/v1/projects/{proj}/credentials") + .pathParam("proj", FOO_PROJ) + .execute(); + assertThat(response.status()).isSameAs(HttpStatus.NO_CONTENT); + response = systemAdminClient.prepare() + .get("/api/v1/projects/{proj}/repos/{repo}/credentials") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", BAR_REPO) + .execute(); + assertThat(response.status()).isSameAs(HttpStatus.NO_CONTENT); + + // Create credentials. + final Map repoCredential = + ImmutableMap.of("type", "access_token", "id", "repo-credential", + "accessToken", "secret-repo-token"); + final ResponseEntity creationResponse = + userClient.prepare() + .post("/api/v1/projects/{proj}/repos/{repo}/credentials") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", BAR_REPO) + .contentJson(repoCredential) + .asJson(PushResultDto.class) + .execute(); + assertThat(creationResponse.status()).isEqualTo(HttpStatus.CREATED); + final Map projectCredential = + ImmutableMap.of("type", "access_token", "id", "project-credential", + "accessToken", "secret-repo-token"); + createProjectCredential(projectCredential); + + // Create mirrors. + final MirrorRequest newMirror1 = + newMirror(BAR_REPO, "mirror-1", repoMirrorCredentialId(FOO_PROJ, BAR_REPO, "repo-credential")); + createMirror(BAR_REPO, newMirror1); + final MirrorRequest newMirror2 = + newMirror(BAR_REPO, "mirror-2", projectMirrorCredentialId(FOO_PROJ, "project-credential")); + createMirror(BAR_REPO, newMirror2); + + // Read mirrors. + final MirrorDto mirror1 = getMirror(BAR_REPO, newMirror1.id()); + assertThat(mirror1.credentialId()).isEqualTo( + repoMirrorCredentialId(FOO_PROJ, BAR_REPO, "repo-credential")); + final MirrorDto mirror2 = getMirror(BAR_REPO, newMirror2.id()); + assertThat(mirror2.credentialId()).isEqualTo( + projectMirrorCredentialId(FOO_PROJ, "project-credential")); + } + + private static void rejectInvalidRepositoryUri() { final MirrorRequest newMirror = new MirrorRequest("invalid-mirror", true, @@ -129,7 +210,7 @@ private void rejectInvalidRepositoryUri() { "/remote-path/1", "mirror-branch", ".my-env0\n.my-env1", - "public-key-credential", + projectMirrorCredentialId(FOO_PROJ, "public-key-credential"), null); final AggregatedHttpResponse response = userClient.prepare() @@ -142,7 +223,7 @@ private void rejectInvalidRepositoryUri() { assertThat(response.contentUtf8()).contains("no host in remoteUri"); } - private void setUpRole() { + private static void setUpRole() { final ResponseEntity res = systemAdminClient.prepare() .post("/api/v1/metadata/{proj}/members") @@ -153,8 +234,8 @@ private void setUpRole() { assertThat(res.status()).isEqualTo(HttpStatus.OK); } - private void createAndReadCredential() { - final List> credentials = ImmutableList.of( + private static void createAndReadCredential() { + final List> credentials = ImmutableList.of( ImmutableMap.of("type", "password", "id", "password-credential", "username", "username-0", "password", "password-0"), ImmutableMap.of("type", "access_token", "id", "access-token-credential", @@ -166,18 +247,9 @@ private void createAndReadCredential() { ImmutableMap.of("type", "none", "id", "non-credential")); for (int i = 0; i < credentials.size(); i++) { - final Map credential = credentials.get(i); - final String credentialId = (String) credential.get("id"); - final ResponseEntity creationResponse = - userClient.prepare() - .post("/api/v1/projects/{proj}/credentials") - .pathParam("proj", FOO_PROJ) - .contentJson(credential) - .responseTimeoutMillis(0) - .asJson(PushResultDto.class) - .execute(); - assertThat(creationResponse.status()).isEqualTo(HttpStatus.CREATED); - assertThat(creationResponse.content().revision().major()).isEqualTo(i + 2); + final Map credential = credentials.get(i); + final String credentialId = credential.get("id"); + createProjectCredential(credential); for (BlockingWebClient client : ImmutableList.of(systemAdminClient, userClient)) { final boolean isSystemAdmin = client == systemAdminClient; @@ -191,7 +263,7 @@ private void createAndReadCredential() { .execute(); final Credential credentialDto = fetchResponse.content(); assertThat(credentialDto.id()).isEqualTo(credentialId); - final String credentialType = (String) credential.get("type"); + final String credentialType = credential.get("type"); if ("password".equals(credentialType)) { final PasswordCredential actual = (PasswordCredential) credentialDto; assertThat(actual.username()).isEqualTo(credential.get("username")); @@ -227,9 +299,21 @@ private void createAndReadCredential() { } } - private void updateCredential() { + private static void createProjectCredential(Map credential) { + final ResponseEntity creationResponse = + userClient.prepare() + .post("/api/v1/projects/{proj}/credentials") + .pathParam("proj", FOO_PROJ) + .contentJson(credential) + .responseTimeoutMillis(0) + .asJson(PushResultDto.class) + .execute(); + assertThat(creationResponse.status()).isEqualTo(HttpStatus.CREATED); + } + + private static void updateCredential() { final String credentialId = "public-key-credential"; - final Map credential = + final Map credential = ImmutableMap.of("type", "public_key", "id", credentialId, "username", "updated-username-2", @@ -256,7 +340,7 @@ private void updateCredential() { .asJson(Credential.class) .execute(); final PublicKeyCredential actual = (PublicKeyCredential) fetchResponse.content(); - assertThat(actual.id()).isEqualTo((String) credential.get("id")); + assertThat(actual.id()).isEqualTo(credential.get("id")); assertThat(actual.username()).isEqualTo(credential.get("username")); assertThat(actual.publicKey()).isEqualTo(credential.get("publicKey")); if (isSystemAdmin) { @@ -269,27 +353,11 @@ private void updateCredential() { } } - private void createAndReadMirror() { + private static void createAndReadMirror(String repoName) { for (int i = 0; i < 3; i++) { - final MirrorRequest newMirror = newMirror("mirror-" + i); - final ResponseEntity response0 = - userClient.prepare() - .post("/api/v1/projects/{proj}/repos/{repo}/mirrors") - .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .contentJson(newMirror) - .asJson(PushResultDto.class) - .execute(); - assertThat(response0.status()).isEqualTo(HttpStatus.CREATED); - final ResponseEntity response1 = - userClient.prepare() - .get("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .pathParam("id", newMirror.id()) - .asJson(MirrorDto.class) - .execute(); - final MirrorDto savedMirror = response1.content(); + final MirrorRequest newMirror = newMirror(repoName, "mirror-" + i); + createMirror(repoName, newMirror); + final MirrorDto savedMirror = getMirror(repoName, newMirror.id()); assertThat(savedMirror) .usingRecursiveComparison() .ignoringFields("allow") @@ -297,60 +365,70 @@ private void createAndReadMirror() { } // Make sure that the mirror with a port number in the remote URL can be created and read. - final MirrorRequest mirrorWithPort = new MirrorRequest("mirror-with-port-3", - true, - FOO_PROJ, - "5 * * * * ?", - "REMOTE_TO_LOCAL", - BAR_REPO, - "/updated/local-path/", - "git+https", - "git.com:922/line/centraldogma-test.git", - "/updated/remote-path/", - "updated-mirror-branch", - ".updated-env", - "public-key-credential", - null); - - final ResponseEntity response0 = + final MirrorRequest mirrorWithPort = new MirrorRequest( + "mirror-with-port-3", + true, + FOO_PROJ, + "5 * * * * ?", + "REMOTE_TO_LOCAL", + repoName, + "/updated/local-path/", + "git+https", + "git.com:922/line/centraldogma-test.git", + "/updated/remote-path/", + "updated-mirror-branch", + ".updated-env", + projectMirrorCredentialId(FOO_PROJ, "public-key-credential"), + null); + createMirror(repoName, mirrorWithPort); + final MirrorDto savedMirror = getMirror(repoName, mirrorWithPort.id()); + assertThat(savedMirror) + .usingRecursiveComparison() + .ignoringFields("allow") + .isEqualTo(mirrorWithPort); + } + + private static void createMirror(String repoName, MirrorRequest newMirror) { + final ResponseEntity response = userClient.prepare() .post("/api/v1/projects/{proj}/repos/{repo}/mirrors") .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .contentJson(mirrorWithPort) + .pathParam("repo", repoName) + .contentJson(newMirror) .asJson(PushResultDto.class) .execute(); - assertThat(response0.status()).isEqualTo(HttpStatus.CREATED); + assertThat(response.status()).isEqualTo(HttpStatus.CREATED); + } + + private static MirrorDto getMirror(String repoName, String mirrorId) { final ResponseEntity response1 = userClient.prepare() .get("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .pathParam("id", mirrorWithPort.id()) + .pathParam("repo", repoName) + .pathParam("id", mirrorId) .asJson(MirrorDto.class) .execute(); - final MirrorDto savedMirror = response1.content(); - assertThat(savedMirror) - .usingRecursiveComparison() - .ignoringFields("allow") - .isEqualTo(mirrorWithPort); + assertThat(response1.status()).isEqualTo(HttpStatus.OK); + return response1.content(); } - private void updateMirror() { - final MirrorRequest mirror = new MirrorRequest("mirror-2", - true, - FOO_PROJ, - "5 * * * * ?", - "REMOTE_TO_LOCAL", - BAR_REPO, - "/updated/local-path/", - "git+https", - "github.com/line/centraldogma-updated.git", - "/updated/remote-path/", - "updated-mirror-branch", - ".updated-env", - "access-token-credential", - null); + private static void updateMirror() { + final MirrorRequest mirror = new MirrorRequest( + "mirror-2", + true, + FOO_PROJ, + "5 * * * * ?", + "REMOTE_TO_LOCAL", + BAR_REPO, + "/updated/local-path/", + "git+https", + "github.com/line/centraldogma-updated.git", + "/updated/remote-path/", + "updated-mirror-branch", + ".updated-env", + projectMirrorCredentialId(FOO_PROJ, "access-token-credential"), + null); final ResponseEntity updateResponse = userClient.prepare() .put("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") @@ -361,73 +439,76 @@ private void updateMirror() { .asJson(PushResultDto.class) .execute(); assertThat(updateResponse.status()).isEqualTo(HttpStatus.OK); - final ResponseEntity fetchResponse = - userClient.prepare() - .get("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .pathParam("id", mirror.id()) - .asJson(MirrorDto.class) - .execute(); - final MirrorDto savedMirror = fetchResponse.content(); + final MirrorDto savedMirror = getMirror(BAR_REPO, mirror.id()); assertThat(savedMirror) .usingRecursiveComparison() .ignoringFields("allow") .isEqualTo(mirror); } - private void deleteMirror() { - final String mirrorId = "mirror-2"; - assertThat(userClient.prepare() - .delete("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .pathParam("id", mirrorId) - .execute() - .status()) - .isEqualTo(HttpStatus.NO_CONTENT); - assertThat(userClient.prepare() - .get("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("repo", BAR_REPO) - .pathParam("id", mirrorId) - .execute() - .status()) - .isEqualTo(HttpStatus.NOT_FOUND); + private static void deleteMirror(String repoName) { + for (int i = 0; i < 3; i++) { + final String mirrorId = "mirror-" + i; + assertThat(userClient.prepare() + .delete("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", repoName) + .pathParam("id", mirrorId) + .execute() + .status()) + .isEqualTo(HttpStatus.NO_CONTENT); + assertThat(userClient.prepare() + .get("/api/v1/projects/{proj}/repos/{repo}/mirrors/{id}") + .pathParam("proj", FOO_PROJ) + .pathParam("repo", repoName) + .pathParam("id", mirrorId) + .execute() + .status()) + .isEqualTo(HttpStatus.NOT_FOUND); + } } - private void deleteCredential() { - final String credentialId = "public-key-credential"; - assertThat(userClient.prepare() - .delete("/api/v1/projects/{proj}/credentials/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("id", credentialId) - .execute() - .status()) - .isEqualTo(HttpStatus.NO_CONTENT); - assertThat(userClient.prepare() - .get("/api/v1/projects/{proj}/credentials/{id}") - .pathParam("proj", FOO_PROJ) - .pathParam("id", credentialId) - .execute() - .status()) - .isEqualTo(HttpStatus.NOT_FOUND); + private static void deleteCredential() { + final Set credentialIds = ImmutableSet.of("password-credential", + "access-token-credential", + "public-key-credential", + "non-credential"); + for (String credentialId : credentialIds) { + assertThat(userClient.prepare() + .delete("/api/v1/projects/{proj}/credentials/{id}") + .pathParam("proj", FOO_PROJ) + .pathParam("id", credentialId) + .execute() + .status()) + .isEqualTo(HttpStatus.NO_CONTENT); + assertThat(userClient.prepare() + .get("/api/v1/projects/{proj}/credentials/{id}") + .pathParam("proj", FOO_PROJ) + .pathParam("id", credentialId) + .execute() + .status()) + .isEqualTo(HttpStatus.NOT_FOUND); + } + } + + private static MirrorRequest newMirror(String repoName, String id) { + return newMirror(repoName, id, projectMirrorCredentialId(FOO_PROJ, "public-key-credential")); } - private static MirrorRequest newMirror(String id) { + private static MirrorRequest newMirror(String repoName, String id, String credentialId) { return new MirrorRequest(id, true, FOO_PROJ, "5 * * * * ?", "REMOTE_TO_LOCAL", - BAR_REPO, + repoName, "/local-path/" + id + '/', "git+https", "github.com/line/centraldogma-authtest.git", "/remote-path/" + id + '/', "mirror-branch", ".my-env0\n.my-env1", - "public-key-credential", + credentialId, null); } } diff --git a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTestUtils.java b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTestUtils.java index f3cf3478d8..128dab45a1 100644 --- a/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTestUtils.java +++ b/server-mirror-git/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTestUtils.java @@ -59,7 +59,7 @@ static T newMirror(String remoteUri, Cron schedule, final Mirror mirror = new GitMirrorProvider().newMirror( new MirrorContext("mirror-id", true, schedule, MirrorDirection.LOCAL_TO_REMOTE, - credential, repository, "/", URI.create(remoteUri), null, null)); + credential, "", repository, "/", URI.create(remoteUri), null, null)); assertThat(mirror).isInstanceOf(mirrorType); assertThat(mirror.direction()).isEqualTo(MirrorDirection.LOCAL_TO_REMOTE); @@ -76,7 +76,8 @@ static void assertMirrorNull(String remoteUri) { final Credential credential = mock(Credential.class); final Mirror mirror = new GitMirrorProvider().newMirror( new MirrorContext("mirror-id", true, EVERY_MINUTE, MirrorDirection.LOCAL_TO_REMOTE, - credential, mock(Repository.class), "/", URI.create(remoteUri), null, null)); + credential, "", mock(Repository.class), "/", URI.create(remoteUri), + null, null)); assertThat(mirror).isNull(); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/CredentialServiceV1.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/CredentialServiceV1.java index 3228d54970..e695ff7634 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/CredentialServiceV1.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/CredentialServiceV1.java @@ -34,6 +34,7 @@ import com.linecorp.centraldogma.common.Author; import com.linecorp.centraldogma.common.Change; import com.linecorp.centraldogma.common.Markup; +import com.linecorp.centraldogma.common.ProjectRole; import com.linecorp.centraldogma.common.RepositoryRole; import com.linecorp.centraldogma.common.Revision; import com.linecorp.centraldogma.internal.api.v1.PushResultDto; @@ -41,10 +42,10 @@ import com.linecorp.centraldogma.server.command.CommandExecutor; import com.linecorp.centraldogma.server.command.CommitResult; import com.linecorp.centraldogma.server.credential.Credential; +import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole; import com.linecorp.centraldogma.server.internal.api.auth.RequiresRepositoryRole; import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager; import com.linecorp.centraldogma.server.metadata.User; -import com.linecorp.centraldogma.server.storage.project.Project; import com.linecorp.centraldogma.server.storage.repository.MetaRepository; import com.linecorp.centraldogma.server.storage.repository.Repository; @@ -66,7 +67,7 @@ public CredentialServiceV1(ProjectApiManager projectApiManager, CommandExecutor * *

Returns the list of the credentials in the project. */ - @RequiresRepositoryRole(value = RepositoryRole.READ, repository = Project.REPO_META) + @RequiresProjectRole(ProjectRole.OWNER) @Get("/projects/{projectName}/credentials") public CompletableFuture> listCredentials(User loginUser, @Param String projectName) { @@ -92,7 +93,7 @@ private static CompletableFuture> maybeMaskSecret( * *

Returns the credential for the ID in the project. */ - @RequiresRepositoryRole(value = RepositoryRole.READ, repository = Project.REPO_META) + @RequiresProjectRole(ProjectRole.OWNER) @Get("/projects/{projectName}/credentials/{id}") public CompletableFuture getCredentialById(User loginUser, @Param String projectName, @Param String id) { @@ -111,7 +112,7 @@ public CompletableFuture getCredentialById(User loginUser, @ConsumesJson @StatusCode(201) @Post("/projects/{projectName}/credentials") - @RequiresRepositoryRole(value = RepositoryRole.WRITE, repository = Project.REPO_META) + @RequiresProjectRole(ProjectRole.OWNER) public CompletableFuture createCredential(@Param String projectName, Credential credential, Author author, User user) { return createOrUpdate(projectName, credential, author, user, false); @@ -124,7 +125,7 @@ public CompletableFuture createCredential(@Param String projectNa */ @ConsumesJson @Put("/projects/{projectName}/credentials/{id}") - @RequiresRepositoryRole(value = RepositoryRole.WRITE, repository = Project.REPO_META) + @RequiresProjectRole(ProjectRole.OWNER) public CompletableFuture updateCredential(@Param String projectName, @Param String id, Credential credential, Author author, User user) { checkArgument(id.equals(credential.id()), "The credential ID (%s) can't be updated", id); @@ -137,7 +138,7 @@ public CompletableFuture updateCredential(@Param String projectNa *

Delete the existing credential. */ @Delete("/projects/{projectName}/credentials/{id}") - @RequiresRepositoryRole(value = RepositoryRole.WRITE, repository = Project.REPO_META) + @RequiresProjectRole(ProjectRole.OWNER) public CompletableFuture deleteCredential(@Param String projectName, @Param String id, Author author, User user) { final MetaRepository metaRepository = metaRepo(projectName, user); diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java index ef25090d66..977afd85e4 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MirroringServiceV1.java @@ -70,7 +70,6 @@ import com.linecorp.centraldogma.server.mirror.MirrorListener; import com.linecorp.centraldogma.server.mirror.MirrorResult; import com.linecorp.centraldogma.server.mirror.MirroringServicePluginConfig; -import com.linecorp.centraldogma.server.storage.project.Project; import com.linecorp.centraldogma.server.storage.repository.MetaRepository; import com.linecorp.centraldogma.server.storage.repository.Repository; @@ -196,7 +195,7 @@ public CompletableFuture updateMirror(@Param String projectName, *

Delete the existing mirror. */ @Delete("/projects/{projectName}/repos/{repoName}/mirrors/{id}") - @RequiresRepositoryRole(value = RepositoryRole.WRITE, repository = Project.REPO_META) + @RequiresRepositoryRole(RepositoryRole.ADMIN) public CompletableFuture deleteMirror(@Param String projectName, Repository repository, @Param String id, Author author) { @@ -253,7 +252,7 @@ private Void notifyMirrorEvent(Mirror mirror, boolean update) { // Mirroring may be a long-running task, so we need to increase the timeout. @RequestTimeout(value = 5, unit = TimeUnit.MINUTES) @Post("/projects/{projectName}/repos/{repoName}/mirrors/{mirrorId}/run") - @RequiresRepositoryRole(value = RepositoryRole.WRITE, repository = Project.REPO_META) + @RequiresRepositoryRole(RepositoryRole.ADMIN) public CompletableFuture runMirror(@Param String projectName, Repository repository, @Param String mirrorId, @@ -306,7 +305,7 @@ private static MirrorDto convertToMirrorDto(String projectName, Mirror mirror, b mirror.remotePath(), mirror.remoteBranch(), mirror.gitignore(), - mirror.credential().id(), mirror.zone(), allowed); + mirror.mirrorCredentialId(), mirror.zone(), allowed); } private MetaRepository metaRepo(String projectName) { diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java index e46fef9d32..0c3e48f1c5 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/AbstractMirror.java @@ -56,6 +56,7 @@ public abstract class AbstractMirror implements Mirror { private final boolean enabled; private final MirrorDirection direction; private final Credential credential; + private final String mirrorCredentialId; private final Repository localRepo; private final String localPath; private final URI remoteRepoUri; @@ -72,13 +73,14 @@ public abstract class AbstractMirror implements Mirror { private final long jitterMillis; protected AbstractMirror(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, - URI remoteRepoUri, String remotePath, String remoteBranch, + Credential credential, String mirrorCredentialId, Repository localRepo, + String localPath, URI remoteRepoUri, String remotePath, String remoteBranch, @Nullable String gitignore, @Nullable String zone) { this.id = requireNonNull(id, "id"); this.enabled = enabled; this.direction = requireNonNull(direction, "direction"); this.credential = requireNonNull(credential, "credential"); + this.mirrorCredentialId = requireNonNull(mirrorCredentialId, "mirrorCredentialId"); this.localRepo = requireNonNull(localRepo, "localRepo"); this.localPath = normalizePath(requireNonNull(localPath, "localPath")); this.remoteRepoUri = requireNonNull(remoteRepoUri, "remoteRepoUri"); @@ -142,6 +144,11 @@ public final Credential credential() { return credential; } + @Override + public String mirrorCredentialId() { + return mirrorCredentialId; + } + @Override public final Repository localRepo() { return localRepo; diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java index a66ab9837a..dd6d41e418 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirror.java @@ -38,12 +38,13 @@ public final class CentralDogmaMirror extends AbstractMirror { private final String remoteRepo; public CentralDogmaMirror(String id, boolean enabled, Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, + Credential credential, String mirrorCredentialId, Repository localRepo, + String localPath, URI remoteRepoUri, String remoteProject, String remoteRepo, String remotePath, @Nullable String gitignore, @Nullable String zone) { // Central Dogma has no notion of 'branch', so we just pass an empty string as a placeholder. - super(id, enabled, schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, - "", gitignore, zone); + super(id, enabled, schedule, direction, credential, mirrorCredentialId, localRepo, localPath, + remoteRepoUri, remotePath, "", gitignore, zone); this.remoteProject = requireNonNull(remoteProject, "remoteProject"); this.remoteRepo = requireNonNull(remoteRepo, "remoteRepo"); diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryService.java index 89d1421e44..531e6fbaf5 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryService.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryService.java @@ -15,6 +15,7 @@ */ package com.linecorp.centraldogma.server.internal.mirror; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.LEGACY_MIRRORS_PATH; import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.mirrorFile; import static com.linecorp.centraldogma.server.storage.project.Project.REPO_META; @@ -141,7 +142,7 @@ private boolean migrateMirrors(MetaRepository repository) throws Exception { if (entries.isEmpty()) { return false; } - + repository.parent().name(); final List> changes = new ArrayList<>(); for (Map.Entry> entry : entries.entrySet()) { final JsonNode content = (JsonNode) entry.getValue().content(); @@ -156,8 +157,10 @@ private boolean migrateMirrors(MetaRepository repository) throws Exception { warnInvalidMirrorConfig(entry, content); continue; } + final MirrorConfig newMirrorConfig = mirrorConfig.withCredentialId( + projectMirrorCredentialId(repository.parent().name(), mirrorConfig.credentialId())); changes.add(Change.ofJsonUpsert(mirrorFile(repoName, mirrorConfig.id()), - Jackson.valueToTree(content))); + Jackson.valueToTree(newMirrorConfig))); } catch (JsonProcessingException e) { warnInvalidMirrorConfig(entry, content); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/CentralDogmaMirrorProvider.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/CentralDogmaMirrorProvider.java index f27c2383a8..c508c26787 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/CentralDogmaMirrorProvider.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/CentralDogmaMirrorProvider.java @@ -60,7 +60,8 @@ public Mirror newMirror(MirrorContext context) { final String remoteRepo = pathMatcher.group(2); final String remotePath = repositoryUri.path(); return new CentralDogmaMirror(context.id(), context.enabled(), context.schedule(), context.direction(), - context.credential(), context.localRepo(), context.localPath(), + context.credential(), context.mirrorCredentialId(), + context.localRepo(), context.localPath(), repositoryUri.uri(), remoteProject, remoteRepo, remotePath, context.gitignore(), context.zone()); } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java index 1b86099139..af546d505e 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepository.java @@ -20,9 +20,12 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.regex.Pattern; @@ -156,7 +159,17 @@ public CompletableFuture mirror(String repoName, String id, Revision rev return CompletableFuture.completedFuture(c.toMirror(parent(), Credential.FALLBACK)); } - return convert(ImmutableList.of(repoName), (repoCredentials, projectCredentials) -> { + final List credentialRepoNames; + final boolean hasProjectCredential; + if (c.repoCredential()) { + credentialRepoNames = ImmutableList.of(repoName); + hasProjectCredential = false; + } else { + credentialRepoNames = ImmutableList.of(); + hasProjectCredential = true; + } + + return convert(credentialRepoNames, hasProjectCredential, (repoCredentials, projectCredentials) -> { final Mirror mirror = c.toMirror(parent(), repoCredentials, projectCredentials); if (mirror == null) { throw mirrorNotFound(revision, mirrorFile); @@ -173,84 +186,90 @@ private EntryNotFoundException mirrorNotFound(Revision revision, String mirrorFi } private CompletableFuture> allMirrors() { - return find("/repos/*/mirrors/*.json").thenCompose(entries -> { - if (entries.isEmpty()) { - return UnmodifiableFuture.completedFuture(ImmutableList.of()); - } - - final List repoNames = entries.keySet().stream() - .map(path -> path.substring(7, path.indexOf('/', 8))) - .distinct() - .collect(toImmutableList()); - return convert(entries, repoNames); - }); + return find(NEW_MIRRORS_PATH + "*.json").thenCompose(this::handleAllMirrors); } private CompletableFuture> allMirrors(String repoName) { - return find("/repos/" + repoName + "/mirrors/*.json").thenCompose(entries -> { - if (entries.isEmpty()) { - return UnmodifiableFuture.completedFuture(ImmutableList.of()); - } - - return convert(entries, ImmutableList.of(repoName)); - }); + return find("/repos/" + repoName + "/mirrors/*.json").thenCompose(this::handleAllMirrors); } - private CompletableFuture> convert( - Map> entries, List repoNames) { - return convert(repoNames, (repoCredentials, projectCredentials) -> { - try { - return parseMirrors(entries, repoCredentials, projectCredentials); - } catch (JsonProcessingException e) { - return Exceptions.throwUnsafely(e); + private CompletableFuture> handleAllMirrors(Map> entries) { + if (entries.isEmpty()) { + return UnmodifiableFuture.completedFuture(ImmutableList.of()); + } + + final Set repoNames = new HashSet<>(); + boolean hasProjectCredential = false; + final List mirrorConfigs = toMirrorConfigs(entries); + for (MirrorConfig mirrorConfig : mirrorConfigs) { + if (mirrorConfig.repoCredential()) { + repoNames.add(mirrorConfig.localRepo()); + } else { + hasProjectCredential = true; } - }); + } + + return convert(new ArrayList<>(repoNames), hasProjectCredential, + (repoCredentials, projectCredentials) -> { + return mirrorConfigs.stream() + .map(mirrorConfig -> mirrorConfig.toMirror( + parent(), repoCredentials, projectCredentials)) + .filter(Objects::nonNull) + .collect(toImmutableList()); + }); } private CompletableFuture convert( - List repoNames, + List credentialRepoNames, + boolean hasProjectCredential, BiFunction>, List, T> convertFunction) { - final ImmutableList.Builder>> builder = ImmutableList.builder(); + final ImmutableList>> repoCredentialFutures; + if (credentialRepoNames.isEmpty()) { + repoCredentialFutures = ImmutableList.of(); + } else { + final ImmutableList.Builder>> builder = ImmutableList.builder(); - for (String repoName : repoNames) { - builder.add(repoCredentials(repoName)); + for (String repoName : credentialRepoNames) { + builder.add(repoCredentials(repoName)); + } + repoCredentialFutures = builder.build(); + } + + final CompletableFuture> projectCredentialsFuture; + if (hasProjectCredential) { + projectCredentialsFuture = projectCredentials(); + } else { + projectCredentialsFuture = UnmodifiableFuture.completedFuture(ImmutableList.of()); } - final ImmutableList>> futures = builder.build(); - final CompletableFuture> projectCredentialsFuture = projectCredentials(); final CompletableFuture allOfFuture = - CompletableFuture.allOf(Stream.concat(futures.stream(), + CompletableFuture.allOf(Stream.concat(repoCredentialFutures.stream(), Stream.of(projectCredentialsFuture)) .toArray(CompletableFuture[]::new)); + return allOfFuture.thenApply(unused -> { final ImmutableMap.Builder> repoCredentialsBuilder = ImmutableMap.builder(); - for (int i = 0; i < repoNames.size(); i++) { - repoCredentialsBuilder.put(repoNames.get(i), futures.get(i).join()); + for (int i = 0; i < credentialRepoNames.size(); i++) { + repoCredentialsBuilder.put(credentialRepoNames.get(i), repoCredentialFutures.get(i).join()); } final List projectCredentials = projectCredentialsFuture.join(); return convertFunction.apply(repoCredentialsBuilder.build(), projectCredentials); }); } - private List parseMirrors(Map> entries, - Map> repoCredentials, - List projectCredentials) - throws JsonProcessingException { + private List toMirrorConfigs(Map> entries) { return entries.entrySet().stream().map(entry -> { final JsonNode mirrorJson = (JsonNode) entry.getValue().content(); if (!mirrorJson.isObject()) { throw newInvalidJsonTypeException(entry.getKey(), mirrorJson); } - final MirrorConfig c; try { - c = Jackson.treeToValue(mirrorJson, MirrorConfig.class); + return Jackson.treeToValue(mirrorJson, MirrorConfig.class); } catch (JsonProcessingException e) { return Exceptions.throwUnsafely(e); } - return c.toMirror(parent(), repoCredentials, projectCredentials); }) - .filter(Objects::nonNull) .collect(toImmutableList()); } @@ -437,21 +456,21 @@ private static void validateMirror(MirrorRequest mirror, @Nullable ZoneConfig zo } } - private static MirrorConfig converterToMirrorConfig(MirrorRequest mirrorDto) { + private static MirrorConfig converterToMirrorConfig(MirrorRequest mirrorRequest) { final String remoteUri = - mirrorDto.remoteScheme() + "://" + mirrorDto.remoteUrl() + - MirrorUtil.normalizePath(mirrorDto.remotePath()) + '#' + mirrorDto.remoteBranch(); + mirrorRequest.remoteScheme() + "://" + mirrorRequest.remoteUrl() + + MirrorUtil.normalizePath(mirrorRequest.remotePath()) + '#' + mirrorRequest.remoteBranch(); return new MirrorConfig( - mirrorDto.id(), - mirrorDto.enabled(), - mirrorDto.schedule(), - MirrorDirection.valueOf(mirrorDto.direction()), - mirrorDto.localRepo(), - mirrorDto.localPath(), + mirrorRequest.id(), + mirrorRequest.enabled(), + mirrorRequest.schedule(), + MirrorDirection.valueOf(mirrorRequest.direction()), + mirrorRequest.localRepo(), + mirrorRequest.localPath(), URI.create(remoteUri), - mirrorDto.gitignore(), - mirrorDto.credentialId(), - mirrorDto.zone()); + mirrorRequest.gitignore(), + mirrorRequest.credentialId(), + mirrorRequest.zone()); } } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java index 45b52e15ba..ee6fc42f20 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/MirrorConfig.java @@ -18,6 +18,9 @@ package com.linecorp.centraldogma.server.internal.storage.repository; import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.PROJECT_CREDENTIAL_ID_PATTERN; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.REPO_CREDENTIAL_ID_PATTERN; import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_DOGMA; import static java.util.Objects.requireNonNull; @@ -25,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.ServiceLoader; +import java.util.regex.Matcher; import javax.annotation.Nullable; @@ -79,6 +83,8 @@ public final class MirrorConfig { @Nullable private final String gitignore; private final String credentialId; + private final boolean repoCredential; + private final String credentialResourceId; @Nullable private final Cron schedule; @Nullable @@ -95,13 +101,17 @@ public MirrorConfig(@JsonProperty("id") String id, @JsonProperty("gitignore") @Nullable Object gitignore, @JsonProperty("credentialId") String credentialId, @JsonProperty("zone") @Nullable String zone) { + this(id, enabled, schedule != null ? CRON_PARSER.parse(schedule) : null, direction, localRepo, + localPath, remoteUri, gitignore, credentialId, zone); + } + + private MirrorConfig(String id, @Nullable Boolean enabled, @Nullable Cron schedule, + MirrorDirection direction, String localRepo, @Nullable String localPath, + URI remoteUri, @Nullable Object gitignore, String credentialId, + @Nullable String zone) { this.id = requireNonNull(id, "id"); this.enabled = firstNonNull(enabled, true); - if (schedule != null) { - this.schedule = CRON_PARSER.parse(schedule); - } else { - this.schedule = null; - } + this.schedule = schedule; this.direction = requireNonNull(direction, "direction"); this.localRepo = requireNonNull(localRepo, "localRepo"); this.localPath = firstNonNull(localPath, "/"); @@ -125,9 +135,37 @@ public MirrorConfig(@JsonProperty("id") String id, this.gitignore = null; } this.credentialId = requireNonNull(credentialId, "credentialId"); + if (isNullOrEmpty(credentialId)) { + // Credential.FALLBACK + repoCredential = false; + credentialResourceId = ""; + } else { + Matcher matcher = REPO_CREDENTIAL_ID_PATTERN.matcher(credentialId); + if (matcher.matches()) { + repoCredential = true; + credentialResourceId = matcher.group(3); + } else { + matcher = PROJECT_CREDENTIAL_ID_PATTERN.matcher(credentialId); + if (matcher.matches()) { + repoCredential = false; + credentialResourceId = matcher.group(2); + } else { + // In the middle of migration from legacy credential to project-credential. + assert !credentialId.contains("/") : credentialId; + repoCredential = false; + credentialResourceId = credentialId; + } + } + } + this.zone = zone; } + public MirrorConfig withCredentialId(String credentialId) { + return new MirrorConfig(id, enabled, schedule, direction, localRepo, localPath, remoteUri, + gitignore, credentialId, zone); + } + @Nullable Mirror toMirror(Project parent, Map> repoCredentials, List projectCredentials) { @@ -135,15 +173,14 @@ Mirror toMirror(Project parent, Map> repoCredentials, return null; } - final Credential credential = findCredential(repoCredentials, projectCredentials, localRepo, - credentialId); + final Credential credential = findCredential(repoCredentials, projectCredentials); return toMirror(parent, credential); } Mirror toMirror(Project parent, Credential credential) { final MirrorContext mirrorContext = new MirrorContext( id, enabled, schedule, direction, - credential, + credential, credential == Credential.FALLBACK ? "" : credentialId, parent.repos().get(localRepo), localPath, remoteUri, gitignore, zone); for (MirrorProvider mirrorProvider : MIRROR_PROVIDERS) { final Mirror mirror = mirrorProvider.newMirror(mirrorContext); @@ -155,24 +192,22 @@ Mirror toMirror(Project parent, Credential credential) { throw new IllegalArgumentException("could not find a mirror provider for " + mirrorContext); } - public static Credential findCredential(Map> repoCredentials, - List projectCredentials, - String repoName, @Nullable String credentialId) { - if (credentialId != null) { - // Repository credentials take precedence over project credentials. - final List credentials = repoCredentials.get(repoName); + private Credential findCredential(Map> repoCredentials, + List projectCredentials) { + if (repoCredential) { + final List credentials = repoCredentials.get(localRepo); if (credentials != null) { for (Credential c : credentials) { final String id = c.id(); - if (credentialId.equals(id)) { + if (credentialResourceId.equals(id)) { return c; } } } - + } else { for (Credential c : projectCredentials) { final String id = c.id(); - if (credentialId.equals(id)) { + if (credentialResourceId.equals(id)) { return c; } } @@ -222,6 +257,10 @@ public String credentialId() { return credentialId; } + public boolean repoCredential() { + return repoCredential; + } + @Nullable @JsonProperty("schedule") public String schedule() { diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java index 61720ef2cf..65e82843bb 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/Mirror.java @@ -63,6 +63,13 @@ public interface Mirror { */ Credential credential(); + /** + * Returns the ID of the credential which is used to access the Git repositories. + * It is in the form of {@code "projects//credentials/"} or + * {@code "projects//repos//credentials/"}. + */ + String mirrorCredentialId(); + /** * Returns the Central Dogma repository where is supposed to keep the mirrored files. */ diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java index 0390b56666..3bba003868 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorContext.java @@ -39,6 +39,7 @@ public final class MirrorContext { private final Cron schedule; private final MirrorDirection direction; private final Credential credential; + private final String mirrorCredentialId; private final Repository localRepo; private final String localPath; private final URI remoteUri; @@ -51,13 +52,14 @@ public final class MirrorContext { * Creates a new instance. */ public MirrorContext(String id, boolean enabled, @Nullable Cron schedule, MirrorDirection direction, - Credential credential, Repository localRepo, String localPath, URI remoteUri, - @Nullable String gitignore, @Nullable String zone) { + Credential credential, String mirrorCredentialId, Repository localRepo, + String localPath, URI remoteUri, @Nullable String gitignore, @Nullable String zone) { this.id = requireNonNull(id, "id"); this.enabled = enabled; this.schedule = schedule; this.direction = requireNonNull(direction, "direction"); this.credential = requireNonNull(credential, "credential"); + this.mirrorCredentialId = requireNonNull(mirrorCredentialId, "mirrorCredentialId"); this.localRepo = requireNonNull(localRepo, "localRepo"); this.localPath = requireNonNull(localPath, "localPath"); this.remoteUri = requireNonNull(remoteUri, "remoteUri"); @@ -102,6 +104,15 @@ public Credential credential() { return credential; } + /** + * Returns the ID of the credential which is used to access the Git repositories. + * It is in the form of {@code "projects//credentials/"} or + * {@code "projects//repos//credentials/"}. + */ + public String mirrorCredentialId() { + return mirrorCredentialId; + } + /** * Returns the local repository of this mirror. */ diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirrorTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirrorTest.java index da64562f60..c12f7ccffb 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirrorTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/CentralDogmaMirrorTest.java @@ -123,7 +123,7 @@ static T newMirror(String remoteUri, Cron schedule, final Mirror mirror = new CentralDogmaMirrorProvider().newMirror( new MirrorContext(mirrorId, true, schedule, MirrorDirection.LOCAL_TO_REMOTE, - credential, repository, "/", URI.create(remoteUri), null, null)); + credential, "", repository, "/", URI.create(remoteUri), null, null)); assertThat(mirror).isInstanceOf(mirrorType); assertThat(mirror.id()).isEqualTo(mirrorId); @@ -141,7 +141,8 @@ static void assertMirrorNull(String remoteUri) { final Credential credential = mock(Credential.class); final Mirror mirror = new CentralDogmaMirrorProvider().newMirror( new MirrorContext("mirror-id", true, EVERY_MINUTE, MirrorDirection.LOCAL_TO_REMOTE, - credential, mock(Repository.class), "/", URI.create(remoteUri), null, null)); + credential, "", mock(Repository.class), "/", URI.create(remoteUri), + null, null)); assertThat(mirror).isNull(); } } diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryServiceTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryServiceTest.java index b0639b88cd..3fd0ed21d9 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryServiceTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MigratingMirrorToRepositoryServiceTest.java @@ -15,6 +15,7 @@ */ package com.linecorp.centraldogma.server.internal.mirror; +import static com.linecorp.centraldogma.internal.api.v1.MirrorRequest.projectMirrorCredentialId; import static com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository.LEGACY_MIRRORS_PATH; import static org.assertj.core.api.Assertions.assertThat; @@ -47,40 +48,37 @@ class MigratingMirrorToRepositoryServiceTest { static final String REPO0_MIRROR_0 = '{' + " \"id\": \"mirror-0\"," + - " \"type\": \"single\"," + " \"enabled\": true," + " \"schedule\": \"0 * * * * ?\"," + " \"direction\": \"REMOTE_TO_LOCAL\"," + " \"localRepo\": \"" + TEST_REPO0 + "\"," + " \"localPath\": \"/\"," + " \"remoteUri\": \"git+ssh://git.foo.com/foo.git/settings#release\"," + - " \"credentialId\": \"credential-1\"" + + " \"credentialId\": \"%s\"" + '}'; static final String REPO0_MIRROR_1 = '{' + " \"id\": \"mirror-1\"," + - " \"type\": \"single\"," + " \"enabled\": true," + " \"schedule\": \"0 * * * * ?\"," + " \"direction\": \"REMOTE_TO_LOCAL\"," + " \"localRepo\": \"" + TEST_REPO0 + "\"," + " \"localPath\": \"/\"," + " \"remoteUri\": \"git+ssh://git.bar.com/foo.git/settings#release\"," + - " \"credentialId\": \"credential-1\"" + + " \"credentialId\": \"%s\"" + '}'; static final String REPO1_MIRROR = '{' + " \"id\": \"mirror-2\"," + - " \"type\": \"single\"," + " \"enabled\": true," + " \"schedule\": \"0 * * * * ?\"," + " \"direction\": \"REMOTE_TO_LOCAL\"," + " \"localRepo\": \"" + TEST_REPO1 + "\"," + " \"localPath\": \"/\"," + " \"remoteUri\": \"git+ssh://git.qux.com/foo.git/settings#release\"," + - " \"credentialId\": \"credential-1\"" + + " \"credentialId\": \"%s\"" + '}'; @RegisterExtension @@ -110,9 +108,12 @@ void migrate() throws Exception { final Project project = projectManager.get(TEST_PROJ); final List> changes = new ArrayList<>(); - changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-0.json", REPO0_MIRROR_0)); - changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-1.json", REPO0_MIRROR_1)); - changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-2.json", REPO1_MIRROR)); + changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-0.json", + String.format(REPO0_MIRROR_0, "credential-1"))); + changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-1.json", + String.format(REPO0_MIRROR_1, "credential-1"))); + changes.add(Change.ofJsonUpsert(LEGACY_MIRRORS_PATH + "mirror-2.json", + String.format(REPO1_MIRROR, "credential-1"))); project.metaRepo().commit(Revision.HEAD, System.currentTimeMillis(), Author.SYSTEM, "Create a legacy mirrors.json", changes).join(); @@ -127,13 +128,19 @@ void migrate() throws Exception { assertThat(entries).containsExactlyInAnyOrderEntriesOf(ImmutableMap.of( "/repos/" + TEST_REPO0 + "/mirrors/mirror-0.json", Entry.ofJson(new Revision(3), - "/repos/" + TEST_REPO0 + "/mirrors/mirror-0.json", REPO0_MIRROR_0), + "/repos/" + TEST_REPO0 + "/mirrors/mirror-0.json", + String.format(REPO0_MIRROR_0, + projectMirrorCredentialId(TEST_PROJ, "credential-1"))), "/repos/" + TEST_REPO0 + "/mirrors/mirror-1.json", Entry.ofJson(new Revision(3), - "/repos/" + TEST_REPO0 + "/mirrors/mirror-1.json", REPO0_MIRROR_1), + "/repos/" + TEST_REPO0 + "/mirrors/mirror-1.json", + String.format(REPO0_MIRROR_1, + projectMirrorCredentialId(TEST_PROJ, "credential-1"))), "/repos/" + TEST_REPO1 + "/mirrors/mirror-2.json", Entry.ofJson(new Revision(3), - "/repos/" + TEST_REPO1 + "/mirrors/mirror-2.json", REPO1_MIRROR) + "/repos/" + TEST_REPO1 + "/mirrors/mirror-2.json", + String.format(REPO1_MIRROR, + projectMirrorCredentialId(TEST_PROJ, "credential-1"))) )); } } diff --git a/webapp/src/dogma/features/api/apiSlice.ts b/webapp/src/dogma/features/api/apiSlice.ts index ed1667d8bf..ccb3e0d7c2 100644 --- a/webapp/src/dogma/features/api/apiSlice.ts +++ b/webapp/src/dogma/features/api/apiSlice.ts @@ -28,7 +28,7 @@ import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { DeleteUserOrTokenRepositoryRoleDto } from 'dogma/features/repo/settings/DeleteUserOrTokenRepositoryRoleDto'; import { AddUserOrTokenRepositoryRoleDto } from 'dogma/features/repo/settings/AddUserOrTokenRepositoryRoleDto'; import { DeleteMemberDto } from 'dogma/features/project/settings/members/DeleteMemberDto'; -import { MirrorDto, MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; +import { MirrorDto, MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto'; import { MirrorResult } from '../mirror/MirrorResult'; import { @@ -335,42 +335,50 @@ export const apiSlice = createApi({ }), invalidatesTags: ['Token'], }), - getMirrors: builder.query({ + getProjectMirrors: builder.query({ query: (projectName) => `/api/v1/projects/${projectName}/mirrors`, providesTags: ['Metadata'], }), - getMirror: builder.query({ - query: ({ projectName, id }) => `/api/v1/projects/${projectName}/mirrors/${id}`, + getMirrors: builder.query({ + query: ({ projectName, repoName }) => `/api/v1/projects/${projectName}/repos/${repoName}/mirrors`, + providesTags: ['Metadata'], + }), + getMirror: builder.query({ + query: ({ projectName, repoName, id }) => + `/api/v1/projects/${projectName}/repos/${repoName}/mirrors/${id}`, providesTags: ['Metadata'], }), // eslint-disable-next-line @typescript-eslint/no-explicit-any addNewMirror: builder.mutation({ query: (mirror) => ({ - url: `/api/v1/projects/${mirror.projectName}/mirrors`, + url: `/api/v1/projects/${mirror.projectName}/repos/${mirror.localRepo}/mirrors`, method: 'POST', body: mirror, }), invalidatesTags: ['Metadata'], }), // eslint-disable-next-line @typescript-eslint/no-explicit-any - updateMirror: builder.mutation({ - query: ({ projectName, id, mirror }) => ({ - url: `/api/v1/projects/${projectName}/mirrors/${id}`, + updateMirror: builder.mutation< + any, + { projectName: string; repoName: string; id: string; mirror: MirrorRequest } + >({ + query: ({ projectName, repoName, id, mirror }) => ({ + url: `/api/v1/projects/${projectName}/repos/${repoName}/mirrors/${id}`, method: 'PUT', body: mirror, }), invalidatesTags: ['Metadata'], }), deleteMirror: builder.mutation({ - query: ({ projectName, id }) => ({ - url: `/api/v1/projects/${projectName}/mirrors/${id}`, + query: ({ projectName, repoName, id }) => ({ + url: `/api/v1/projects/${projectName}/repos/${repoName}/mirrors/${id}`, method: 'DELETE', }), invalidatesTags: ['Metadata'], }), - runMirror: builder.mutation({ - query: ({ projectName, id }) => ({ - url: `/api/v1/projects/${projectName}/mirrors/${id}/run`, + runMirror: builder.mutation({ + query: ({ projectName, repoName, id }) => ({ + url: `/api/v1/projects/${projectName}/repos/${repoName}/mirrors/${id}/run`, method: 'POST', }), invalidatesTags: ['Metadata'], @@ -414,7 +422,7 @@ export const apiSlice = createApi({ }), invalidatesTags: ['Mirror'], }), - getCredentials: builder.query({ + getProjectCredentials: builder.query({ query: (projectName) => `/api/v1/projects/${projectName}/credentials`, providesTags: ['Metadata'], }), @@ -532,6 +540,7 @@ export const { useGetHistoryQuery, useGetNormalisedRevisionQuery, // Mirror + useGetProjectMirrorsQuery, useGetMirrorsQuery, useGetMirrorQuery, useAddNewMirrorMutation, @@ -545,7 +554,7 @@ export const { useAddNewMirrorAccessControlMutation, useDeleteMirrorAccessControlMutation, // Credential - useGetCredentialsQuery, + useGetProjectCredentialsQuery, useGetCredentialQuery, useAddNewCredentialMutation, useUpdateCredentialMutation, diff --git a/webapp/src/dogma/features/mirror/RunMirrorButton.tsx b/webapp/src/dogma/features/mirror/RunMirrorButton.tsx index fd15fcfb2f..ca16c10425 100644 --- a/webapp/src/dogma/features/mirror/RunMirrorButton.tsx +++ b/webapp/src/dogma/features/mirror/RunMirrorButton.tsx @@ -21,7 +21,7 @@ import { SerializedError } from '@reduxjs/toolkit'; import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; import { MirrorResult } from './MirrorResult'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; import { Button, ButtonGroup, @@ -50,7 +50,11 @@ export const RunMirror = ({ mirror, children }: RunMirrorProps) => { const onClick = async () => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response: any = await runMirror({ projectName: mirror.projectName, id: mirror.id }).unwrap(); + const response: any = await runMirror({ + projectName: mirror.projectName, + repoName: mirror.localRepo, + id: mirror.id, + }).unwrap(); if ((response as { error: FetchBaseQueryError | SerializedError }).error) { throw (response as { error: FetchBaseQueryError | SerializedError }).error; } diff --git a/webapp/src/dogma/features/repo/settings/RepositorySettingsView.tsx b/webapp/src/dogma/features/repo/settings/RepositorySettingsView.tsx index ca190f614a..9f51a4d758 100644 --- a/webapp/src/dogma/features/repo/settings/RepositorySettingsView.tsx +++ b/webapp/src/dogma/features/repo/settings/RepositorySettingsView.tsx @@ -35,7 +35,7 @@ interface RepositorySettingsViewProps { children?: (meta: ProjectMetadataDto) => ReactNode; } -type TabName = 'users' | 'roles' | 'tokens' | 'credentials' | 'danger zone'; +type TabName = 'users' | 'roles' | 'tokens' | 'mirrors' | 'credentials' | 'danger zone'; export interface TapInfo { name: TabName; @@ -49,6 +49,7 @@ const TABS: TapInfo[] = [ { name: 'roles', path: '', accessRole: 'ADMIN', allowAnonymous: false }, { name: 'users', path: 'users', accessRole: 'ADMIN', allowAnonymous: false }, { name: 'tokens', path: 'tokens', accessRole: 'ADMIN', allowAnonymous: false }, + { name: 'mirrors', path: 'mirrors', accessRole: 'ADMIN', allowAnonymous: true }, { name: 'credentials', path: 'credentials', accessRole: 'ADMIN', allowAnonymous: true }, { name: 'danger zone', path: 'danger-zone', accessRole: 'ADMIN', allowAnonymous: true }, ]; diff --git a/webapp/src/dogma/features/project/settings/mirrors/DeleteMirror.tsx b/webapp/src/dogma/features/repo/settings/mirrors/DeleteMirror.tsx similarity index 86% rename from webapp/src/dogma/features/project/settings/mirrors/DeleteMirror.tsx rename to webapp/src/dogma/features/repo/settings/mirrors/DeleteMirror.tsx index 7a68e34499..728d890bc0 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/DeleteMirror.tsx +++ b/webapp/src/dogma/features/repo/settings/mirrors/DeleteMirror.tsx @@ -7,20 +7,22 @@ import { DeleteConfirmationModal } from 'dogma/common/components/DeleteConfirmat export const DeleteMirror = ({ projectName, + repoName, id, deleteMirror, isLoading, }: { projectName: string; + repoName: string; id: string; - deleteMirror: (projectName: string, id: string) => Promise; + deleteMirror: (projectName: string, repoName: string, id: string) => Promise; isLoading: boolean; }): JSX.Element => { const { isOpen, onToggle, onClose } = useDisclosure(); const dispatch = useAppDispatch(); const handleDelete = async () => { try { - await deleteMirror(projectName, id); + await deleteMirror(projectName, repoName, id); dispatch(newNotification('Mirror deleted.', `Successfully deleted ${id}`, 'success')); onClose(); } catch (error) { @@ -38,6 +40,7 @@ export const DeleteMirror = ({ id={id} type={'mirror'} projectName={projectName} + repoName={repoName} handleDelete={handleDelete} isLoading={isLoading} /> diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx b/webapp/src/dogma/features/repo/settings/mirrors/MirrorForm.tsx similarity index 85% rename from webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx rename to webapp/src/dogma/features/repo/settings/mirrors/MirrorForm.tsx index ba2353d440..b9e96467a5 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorForm.tsx +++ b/webapp/src/dogma/features/repo/settings/mirrors/MirrorForm.tsx @@ -42,20 +42,23 @@ import { GiMirrorMirror, GiPowerButton } from 'react-icons/gi'; import { BiTimer } from 'react-icons/bi'; import { ExternalLinkIcon } from '@chakra-ui/icons'; import { GoArrowBoth, GoArrowDown, GoArrowUp, GoKey, GoRepo } from 'react-icons/go'; -import { Select } from 'chakra-react-select'; +import { GroupBase, Select } from 'chakra-react-select'; import { IoBanSharp } from 'react-icons/io5'; -import { useGetCredentialsQuery, useGetMirrorConfigQuery, useGetReposQuery } from 'dogma/features/api/apiSlice'; +import { + useGetMirrorConfigQuery, + useGetProjectCredentialsQuery, + useGetRepoCredentialsQuery, +} from 'dogma/features/api/apiSlice'; import React, { useMemo, useState } from 'react'; import FieldErrorMessage from 'dogma/common/components/form/FieldErrorMessage'; -import { RepoDto } from 'dogma/features/repo/RepoDto'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto'; -import { FiBox } from 'react-icons/fi'; import cronstrue from 'cronstrue'; import { CiLocationOn } from 'react-icons/ci'; interface MirrorFormProps { projectName: string; + repoName: string; defaultValue: MirrorRequest; onSubmit: ( mirror: MirrorRequest, @@ -75,9 +78,15 @@ const MIRROR_SCHEMES: OptionType[] = ['git+ssh', 'git+http', 'git+https'].map((s label: scheme, })); -const INTERNAL_REPOS = new Set(['dogma', 'meta']); +function repoMirrorCredentialId(project: string, repo: string, id: string): string { + return `projects/${project}/repos/${repo}/credentials/${id}`; +} + +function projectMirrorCredentialId(project: string, id: string): string { + return `projects/${project}/credentials/${id}`; +} -const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: MirrorFormProps) => { +const MirrorForm = ({ projectName, repoName, defaultValue, onSubmit, isWaitingResponse }: MirrorFormProps) => { const { register, handleSubmit, @@ -91,27 +100,41 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: }); const isNew = defaultValue.id === ''; - const { data: repos } = useGetReposQuery(projectName); - const { data: credentials } = useGetCredentialsQuery(projectName); + const { data: projectCredentials } = useGetProjectCredentialsQuery(projectName); + const { data: repoCredentials } = useGetRepoCredentialsQuery({ + projectName: projectName as string, + repoName, + }); const { data: zoneConfig } = useGetMirrorConfigQuery(); const [isScheduleEnabled, setScheduleEnabled] = useState(defaultValue.schedule != null); const schedule = watch('schedule'); - const repoOptions: OptionType[] = (repos || []) - .filter((repo: RepoDto) => !INTERNAL_REPOS.has(repo.name)) - .map((repo: RepoDto) => ({ - value: repo.name, - label: repo.name, + const repoCredentialOptions: OptionType[] = (repoCredentials || []) + .filter((credential: CredentialDto) => credential.id) + .map((credential: CredentialDto) => ({ + label: credential.id, + value: repoMirrorCredentialId(projectName, repoName, credential.id), })); - const credentialOptions: OptionType[] = (credentials || []) + const projectCredentialOptions: OptionType[] = (projectCredentials || []) .filter((credential: CredentialDto) => credential.id) .map((credential: CredentialDto) => ({ - value: credential.id, label: credential.id, + value: projectMirrorCredentialId(projectName, credential.id), })); + const groupedCredentialOptions: GroupBase[] = [ + { + label: 'Repository Credentials', + options: repoCredentialOptions, + }, + { + label: 'Project Credentials', + options: projectCredentialOptions, + }, + ]; + const zoneOptions: OptionType[] = (zoneConfig?.zonePinned ? zoneConfig.zone.allZones : []).map( (zone: string) => ({ value: zone, @@ -161,8 +184,8 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: {isNew ? 'New Mirror' : 'Edit Mirror'} - - {projectName} + + {repoName} @@ -279,40 +302,8 @@ const MirrorForm = ({ projectName, defaultValue, onSubmit, isWaitingResponse }: - - - - - ( - option.value === value) || null} + value={ + groupedCredentialOptions + .flatMap((group) => group.options) + .find((option) => option.value === value) || null + } onChange={(option) => onChange(option?.value || '')} placeholder={ - credentialOptions.length === 0 + repoCredentialOptions.length === 0 && projectCredentialOptions.length === 0 ? 'No credential is found. You need to create credentials first.' : 'Enter credential ID ...' } diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx b/webapp/src/dogma/features/repo/settings/mirrors/MirrorList.tsx similarity index 76% rename from webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx rename to webapp/src/dogma/features/repo/settings/mirrors/MirrorList.tsx index 6bd1389b2c..2612669dd3 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorList.tsx +++ b/webapp/src/dogma/features/repo/settings/mirrors/MirrorList.tsx @@ -1,22 +1,43 @@ import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; import React, { useMemo } from 'react'; import { DataTableClientPagination } from 'dogma/common/components/table/DataTableClientPagination'; -import { useDeleteMirrorMutation, useGetMirrorsQuery } from 'dogma/features/api/apiSlice'; +import { + useDeleteMirrorMutation, + useGetProjectMirrorsQuery, + useGetMirrorsQuery, +} from 'dogma/features/api/apiSlice'; import { Badge, Button, Code, HStack, Link, Tooltip, Wrap, WrapItem } from '@chakra-ui/react'; import { GoRepo } from 'react-icons/go'; import { LabelledIcon } from 'dogma/common/components/LabelledIcon'; -import { MirrorDto } from 'dogma/features/project/settings/mirrors/MirrorRequest'; -import { RunMirror } from '../../../mirror/RunMirrorButton'; +import { MirrorDto } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; +import { RunMirror } from 'dogma/features/mirror/RunMirrorButton'; import { FaPlay } from 'react-icons/fa'; -import { DeleteMirror } from 'dogma/features/project/settings/mirrors/DeleteMirror'; +import { DeleteMirror } from 'dogma/features/repo/settings/mirrors/DeleteMirror'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export type MirrorListProps = { projectName: string; + repoName?: string; }; -const MirrorList = ({ projectName }: MirrorListProps) => { - const { data } = useGetMirrorsQuery(projectName); +const useGetMirrors = (projectName: string, repoName?: string): { data: MirrorDto[]; isLoading: boolean } => { + const { data: projectMirrors = [], isLoading: isProjectLoading } = useGetProjectMirrorsQuery(projectName, { + skip: !!repoName, // Skip if repoName is provided + }); + + const { data: repoMirrors = [], isLoading: isRepoLoading } = useGetMirrorsQuery( + { projectName, repoName: repoName! }, + { skip: !repoName }, // Skip if repoName is not provided + ); + + return { + data: repoName ? repoMirrors : projectMirrors, + isLoading: repoName ? isRepoLoading : isProjectLoading, + }; +}; + +const MirrorList = ({ projectName, repoName }: MirrorListProps) => { + const { data } = useGetMirrors(projectName, repoName); const [deleteMirror, { isLoading }] = useDeleteMirrorMutation(); const columnHelper = createColumnHelper(); const columns = useMemo( @@ -26,7 +47,7 @@ const MirrorList = ({ projectName }: MirrorListProps) const id = info.getValue(); return ( {id ?? 'unknown'} @@ -111,6 +132,7 @@ const MirrorList = ({ projectName }: MirrorListProps) deleteMirror({ projectName, id }).unwrap()} isLoading={isLoading} diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorRequest.ts b/webapp/src/dogma/features/repo/settings/mirrors/MirrorRequest.ts similarity index 100% rename from webapp/src/dogma/features/project/settings/mirrors/MirrorRequest.ts rename to webapp/src/dogma/features/repo/settings/mirrors/MirrorRequest.ts diff --git a/webapp/src/dogma/features/project/settings/mirrors/MirrorView.tsx b/webapp/src/dogma/features/repo/settings/mirrors/MirrorView.tsx similarity index 72% rename from webapp/src/dogma/features/project/settings/mirrors/MirrorView.tsx rename to webapp/src/dogma/features/repo/settings/mirrors/MirrorView.tsx index f3ad5c5e5f..593ae24fbd 100644 --- a/webapp/src/dogma/features/project/settings/mirrors/MirrorView.tsx +++ b/webapp/src/dogma/features/repo/settings/mirrors/MirrorView.tsx @@ -24,11 +24,9 @@ import { EditIcon } from '@chakra-ui/icons'; import React, { ReactNode } from 'react'; import { IconType } from 'react-icons'; import { VscMirror, VscRepoClone } from 'react-icons/vsc'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; -import { CredentialDto } from 'dogma/features/project/settings/credentials/CredentialDto'; -import { FiBox } from 'react-icons/fi'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; import cronstrue from 'cronstrue'; -import { RunMirror } from '../../../mirror/RunMirrorButton'; +import { RunMirror } from 'dogma/features/mirror/RunMirrorButton'; import { FaPlay } from 'react-icons/fa'; import { CiLocationOn } from 'react-icons/ci'; @@ -40,13 +38,22 @@ const HeadRow = ({ children }: { children: ReactNode }) => ( const AlignedIcon = ({ as }: { as: IconType }) => ; +const PROJECT_CREDENTIAL_PATTERN = /^projects\/[^/]+\/credentials\//; + +const isProjectCredential = (credentialId: string) => PROJECT_CREDENTIAL_PATTERN.test(credentialId); +const removeProjectCredentialPrefix = (credentialId: string) => + credentialId.replace(PROJECT_CREDENTIAL_PATTERN, ''); + +const removeRepoCredentialPrefix = (credentialId: string) => + credentialId.replace(/^projects\/[^/]+\/repos\/[^/]+\/credentials\//, ''); + interface MirrorViewProps { projectName: string; + repoName: string; mirror: MirrorRequest; - credential: CredentialDto; } -const MirrorView = ({ projectName, mirror, credential }: MirrorViewProps) => { +const MirrorView = ({ projectName, repoName, mirror }: MirrorViewProps) => { return (

@@ -64,9 +71,9 @@ const MirrorView = ({ projectName, mirror, credential }: MirrorViewProps) => { - Project + Repository - {projectName} + {repoName} @@ -103,11 +110,9 @@ const MirrorView = ({ projectName, mirror, credential }: MirrorViewProps) => { Local path - + - dogma://{projectName}/{mirror.localRepo} + dogma://{projectName}/{repoName} {mirror.localPath} @@ -129,11 +134,20 @@ const MirrorView = ({ projectName, mirror, credential }: MirrorViewProps) => { Credential - {credential && ( - - {mirror.credentialId} - - )} + {mirror.credentialId.length !== 0 && + (isProjectCredential(mirror.credentialId) ? ( + + {removeProjectCredentialPrefix(mirror.credentialId)} (project credential) + + ) : ( + + {removeRepoCredentialPrefix(mirror.credentialId)} (repository credential) + + ))} {mirror.zone && ( @@ -169,7 +183,10 @@ const MirrorView = ({ projectName, mirror, credential }: MirrorViewProps) => { - + diff --git a/webapp/src/pages/api/v1/projects/[projectName]/mirrors/[id]/index.ts b/webapp/src/pages/api/v1/projects/[projectName]/mirrors/[id]/index.ts index 428c6a881d..8d100389f0 100644 --- a/webapp/src/pages/api/v1/projects/[projectName]/mirrors/[id]/index.ts +++ b/webapp/src/pages/api/v1/projects/[projectName]/mirrors/[id]/index.ts @@ -15,7 +15,7 @@ */ import { NextApiRequest, NextApiResponse } from 'next'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; const mirrors: Map = new Map(); diff --git a/webapp/src/pages/api/v1/projects/[projectName]/mirrors/index.ts b/webapp/src/pages/api/v1/projects/[projectName]/mirrors/index.ts index af5f976990..904f9aa25a 100644 --- a/webapp/src/pages/api/v1/projects/[projectName]/mirrors/index.ts +++ b/webapp/src/pages/api/v1/projects/[projectName]/mirrors/index.ts @@ -15,7 +15,7 @@ */ import { NextApiRequest, NextApiResponse } from 'next'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; let mirrors: MirrorRequest[] = []; for (let i = 0; i < 10; i++) { diff --git a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/[id]/edit/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/[id]/edit/index.tsx similarity index 70% rename from webapp/src/pages/app/projects/[projectName]/settings/mirrors/[id]/edit/index.tsx rename to webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/[id]/edit/index.tsx index a53be883ab..0fa9278f74 100644 --- a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/[id]/edit/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/[id]/edit/index.tsx @@ -24,28 +24,44 @@ import { newNotification } from 'dogma/features/notification/notificationSlice'; import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; import { Breadcrumbs } from 'dogma/common/components/Breadcrumbs'; import React from 'react'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; -import MirrorForm from 'dogma/features/project/settings/mirrors/MirrorForm'; +import { MirrorDto, MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; +import MirrorForm from 'dogma/features/repo/settings/mirrors/MirrorForm'; const MirrorEditPage = () => { const router = useRouter(); const projectName = router.query.projectName as string; + const repoName = router.query.repoName as string; const id = router.query.id as string; - const { data, isLoading: isMirrorLoading, error } = useGetMirrorQuery({ projectName, id }); + const { data, isLoading: isMirrorLoading, error } = useGetMirrorQuery({ projectName, repoName, id }); const [updateMirror, { isLoading: isWaitingMutationResponse }] = useUpdateMirrorMutation(); const dispatch = useAppDispatch(); - const onSubmit = async (mirror: MirrorRequest, onSuccess: () => void) => { + const onSubmit = async (mirror: MirrorRequest | MirrorDto, onSuccess: () => void) => { try { - mirror.projectName = projectName; - const response = await updateMirror({ projectName, id, mirror }).unwrap(); + let mirrorRequest: MirrorRequest; + + if ('allow' in mirror) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { allow, ...rest } = mirror; + mirrorRequest = rest; + } else { + mirrorRequest = mirror; + } + + mirrorRequest.projectName = projectName; + mirrorRequest.localRepo = repoName; + + mirrorRequest.projectName = projectName; + mirrorRequest.localRepo = repoName; + + const response = await updateMirror({ projectName, repoName, id, mirror: mirrorRequest }).unwrap(); if ((response as { error: FetchBaseQueryError | SerializedError }).error) { throw (response as { error: FetchBaseQueryError | SerializedError }).error; } dispatch(newNotification(`Mirror '${mirror.id}' is updated`, `Successfully updated`, 'success')); onSuccess(); - Router.push(`/app/projects/${projectName}/settings/mirrors/${id}`); + Router.push(`/app/projects/${projectName}/repos/${repoName}/settings/mirrors/${id}`); } catch (error) { dispatch(newNotification(`Failed to update the mirror`, ErrorMessageParser.parse(error), 'error')); } @@ -57,6 +73,7 @@ const MirrorEditPage = () => { { const router = useRouter(); const projectName = router.query.projectName as string; + const repoName = router.query.repoName as string; const id = router.query.id as string; const { data: mirror, isLoading: isMirrorLoading, error: mirrorError, - } = useGetMirrorQuery({ projectName, id }); - const { - data: credentials, - isLoading: isCredentialLoading, - error: credentialError, - } = useGetCredentialsQuery(projectName); - const credential = (credentials || []).find((credential: CredentialDto) => { - return credential.id === mirror?.credentialId; - }); + } = useGetMirrorQuery({ projectName, repoName, id }); return ( - + {() => { return ( <> - + ); }} diff --git a/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/index.tsx new file mode 100644 index 0000000000..e6c200099c --- /dev/null +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { useRouter } from 'next/router'; +import { Button, Flex, Spacer } from '@chakra-ui/react'; +import Link from 'next/link'; +import { AiOutlinePlus } from 'react-icons/ai'; +import RepositorySettingsView from 'dogma/features/repo/settings/RepositorySettingsView'; +import MirrorList from 'dogma/features/repo/settings/mirrors/MirrorList'; + +const RepoMirrorPage = () => { + const router = useRouter(); + const projectName = router.query.projectName ? (router.query.projectName as string) : ''; + const repoName = router.query.repoName ? (router.query.repoName as string) : ''; + return ( + + {() => ( + <> + + + + + + + )} + + ); +}; + +export default RepoMirrorPage; diff --git a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/new/index.tsx b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/new/index.tsx similarity index 88% rename from webapp/src/pages/app/projects/[projectName]/settings/mirrors/new/index.tsx rename to webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/new/index.tsx index f4acd7f1f6..b3062a336d 100644 --- a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/new/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/repos/[repoName]/settings/mirrors/new/index.tsx @@ -24,12 +24,13 @@ import Router, { useRouter } from 'next/router'; import ErrorMessageParser from 'dogma/features/services/ErrorMessageParser'; import { Breadcrumbs } from 'dogma/common/components/Breadcrumbs'; import React from 'react'; -import { MirrorRequest } from 'dogma/features/project/settings/mirrors/MirrorRequest'; -import MirrorForm from 'dogma/features/project/settings/mirrors/MirrorForm'; +import { MirrorRequest } from 'dogma/features/repo/settings/mirrors/MirrorRequest'; +import MirrorForm from 'dogma/features/repo/settings/mirrors/MirrorForm'; const NewMirrorPage = () => { const router = useRouter(); const projectName = router.query.projectName ? (router.query.projectName as string) : ''; + const repoName = router.query.repoName ? (router.query.repoName as string) : ''; const emptyMirror: MirrorRequest = { id: '', @@ -57,6 +58,7 @@ const NewMirrorPage = () => { ) => { try { formData.projectName = projectName; + formData.localRepo = repoName; if (formData.remoteScheme.startsWith('git') && !formData.remoteUrl.endsWith('.git')) { setError('remoteUrl', { type: 'manual', message: "The remote path must end with '.git'" }); return; @@ -68,7 +70,7 @@ const NewMirrorPage = () => { } dispatch(newNotification('New mirror is created', `Successfully created`, 'success')); onSuccess(); - Router.push(`/app/projects/${projectName}/settings/mirrors`); + Router.push(`/app/projects/${projectName}/repos/${repoName}/settings/mirrors`); } catch (error) { dispatch(newNotification(`Failed to create a new mirror`, ErrorMessageParser.parse(error), 'error')); } @@ -77,9 +79,9 @@ const NewMirrorPage = () => { return ( <> - { const router = useRouter(); const projectName = router.query.projectName ? (router.query.projectName as string) : ''; - const { data: credentialsData } = useGetCredentialsQuery(projectName); + const { data: credentialsData } = useGetProjectCredentialsQuery(projectName); const [deleteCredentialMutation, { isLoading }] = useDeleteCredentialMutation(); return ( diff --git a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/index.tsx b/webapp/src/pages/app/projects/[projectName]/settings/mirrors/index.tsx index 45366945ec..bb60ee9212 100644 --- a/webapp/src/pages/app/projects/[projectName]/settings/mirrors/index.tsx +++ b/webapp/src/pages/app/projects/[projectName]/settings/mirrors/index.tsx @@ -15,11 +15,8 @@ */ import { useRouter } from 'next/router'; -import { Button, Flex, Spacer } from '@chakra-ui/react'; -import Link from 'next/link'; -import { AiOutlinePlus } from 'react-icons/ai'; import ProjectSettingsView from 'dogma/features/project/settings/ProjectSettingsView'; -import MirrorList from 'dogma/features/project/settings/mirrors/MirrorList'; +import MirrorList from 'dogma/features/repo/settings/mirrors/MirrorList'; const ProjectMirrorPage = () => { const router = useRouter(); @@ -28,18 +25,6 @@ const ProjectMirrorPage = () => { {() => ( <> - - - - )}