diff --git a/it/build.gradle b/it/build.gradle index 2e9749a322..eb75c643b3 100644 --- a/it/build.gradle +++ b/it/build.gradle @@ -1,4 +1,5 @@ dependencies { + testImplementation libs.armeria.junit5 // jGit testImplementation libs.jgit // JSch diff --git a/it/src/test/java/com/linecorp/centraldogma/it/mirror/http/HttpMirrorTest.java b/it/src/test/java/com/linecorp/centraldogma/it/mirror/http/HttpMirrorTest.java new file mode 100644 index 0000000000..1fa5ef9bd2 --- /dev/null +++ b/it/src/test/java/com/linecorp/centraldogma/it/mirror/http/HttpMirrorTest.java @@ -0,0 +1,186 @@ +/* + * 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. + */ +package com.linecorp.centraldogma.it.mirror.http; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import javax.annotation.Nullable; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.google.common.base.Strings; + +import com.linecorp.armeria.common.ContentTooLargeException; +import com.linecorp.armeria.server.HttpStatusException; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.annotation.Get; +import com.linecorp.armeria.server.annotation.Param; +import com.linecorp.armeria.server.annotation.StatusCode; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import com.linecorp.centraldogma.client.CentralDogma; +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Entry; +import com.linecorp.centraldogma.common.EntryNotFoundException; +import com.linecorp.centraldogma.server.CentralDogmaBuilder; +import com.linecorp.centraldogma.server.MirrorException; +import com.linecorp.centraldogma.server.MirroringService; +import com.linecorp.centraldogma.server.storage.project.Project; +import com.linecorp.centraldogma.testing.internal.TestUtil; +import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension; + +class HttpMirrorTest { + + private static final int MAX_NUM_BYTES = 1024; // 1 KiB + + private static final String REPO_FOO = "foo"; + + @RegisterExtension + static final CentralDogmaExtension dogma = new CentralDogmaExtension() { + @Override + protected void configure(CentralDogmaBuilder builder) { + builder.mirroringEnabled(true); + builder.maxNumBytesPerMirror(MAX_NUM_BYTES); + } + }; + + @RegisterExtension + static final ServerExtension server = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) throws Exception { + sb.annotatedService(new Object() { + @Get("/get/:length") + public String get(@Param int length) { + return Strings.repeat(".", length); + } + + @Get("/204") // Generate a '204 No Content' response. + public void respond204() {} + + @Get("/304") + @StatusCode(304) + public void respond304() {} + }); + } + }; + + private static CentralDogma client; + private static MirroringService mirroringService; + + @BeforeAll + static void init() { + client = dogma.client(); + mirroringService = dogma.mirroringService(); + } + + private String projName; + + @BeforeEach + void initDogmaRepo(TestInfo testInfo) { + projName = TestUtil.normalizedDisplayName(testInfo); + client.createProject(projName).join(); + client.createRepository(projName, REPO_FOO).join(); + } + + @AfterEach + void destroyDogmaRepo() { + client.removeProject(projName).join(); + } + + @Test + void simple() throws Exception { + // Configure the server to mirror http://.../get/7 into /bar.txt. + pushMirrorSettings(REPO_FOO, "/bar.txt", 7); + testSuccessfulMirror(".......\n"); + } + + @Test + void tooLargeContent() throws Exception { + // Configure the server to mirror http://.../get/ into /bar.txt. + pushMirrorSettings(REPO_FOO, "/bar.txt", MAX_NUM_BYTES + 1); + testFailedMirror(ContentTooLargeException.class); + } + + @Test + void shouldHandle204() throws Exception { + // Configure the server to mirror http://.../204 into /bar.txt. + pushMirrorSettings(REPO_FOO, "/bar.txt", server.httpUri() + "/204"); + testSuccessfulMirror(""); + } + + @Test + void shouldRejectNon2xx() throws Exception { + // Configure the server to mirror http://.../get/ into /bar.txt. + pushMirrorSettings(REPO_FOO, "/bar.txt", server.httpUri() + "/304"); + testFailedMirror(HttpStatusException.class); + } + + private void pushMirrorSettings(String localRepo, @Nullable String localPath, int length) { + pushMirrorSettings(localRepo, localPath, remoteUri(length)); + } + + private void pushMirrorSettings(String localRepo, @Nullable String localPath, String remoteUri) { + client.forRepo(projName, Project.REPO_META) + .commit("Add /mirrors.json", + Change.ofJsonUpsert("/mirrors.json", + "[{" + + " \"type\": \"single\"," + + " \"direction\": \"REMOTE_TO_LOCAL\"," + + " \"localRepo\": \"" + localRepo + "\"," + + " \"localPath\": \"" + firstNonNull(localPath, "/") + "\"," + + " \"remoteUri\": \"" + remoteUri + "\"," + + " \"schedule\": \"0 0 0 1 1 ? 2099\"" + + "}]")) + .push().join(); + } + + private void testSuccessfulMirror(String expectedContent) { + // Trigger the mirroring task. + mirroringService.mirror().join(); + + // On successful mirroring, /bar.txt should contain 7 periods. + final Entry entry = client.forRepo(projName, REPO_FOO) + .file("/bar.txt") + .get().join(); + + assertThat(entry.contentAsText()).isEqualTo(expectedContent); + } + + private void testFailedMirror(Class rootCause) { + // Trigger the mirroring task, which will fail because the response was too large. + assertThatThrownBy(() -> mirroringService.mirror().join()).cause().satisfies(cause -> { + assertThat(cause).isInstanceOf(MirrorException.class) + .hasCauseInstanceOf(rootCause); + }); + + // As a result, /bar.txt shouldn't exist. + assertThatThrownBy(() -> { + client.forRepo(projName, REPO_FOO) + .file("/bar.txt") + .get().join(); + }).hasCauseInstanceOf(EntryNotFoundException.class); + } + + private static String remoteUri(int length) { + return String.format("%s/get/%d", server.httpUri(), length); + } +} 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 ba6e511039..99a9753b0f 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 @@ -52,7 +52,8 @@ public abstract class AbstractMirror implements Mirror { private final Repository localRepo; private final String localPath; private final URI remoteRepoUri; - private final String remotePath; + @Nullable + private final String remoteSubpath; @Nullable private final String remoteBranch; @Nullable @@ -62,16 +63,16 @@ public abstract class AbstractMirror implements Mirror { protected AbstractMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential, Repository localRepo, String localPath, - URI remoteRepoUri, String remotePath, @Nullable String remoteBranch, + URI remoteRepoUri, @Nullable String remoteSubpath, @Nullable String remoteBranch, @Nullable String gitignore) { this.schedule = requireNonNull(schedule, "schedule"); this.direction = requireNonNull(direction, "direction"); this.credential = requireNonNull(credential, "credential"); this.localRepo = requireNonNull(localRepo, "localRepo"); - this.localPath = normalizePath(requireNonNull(localPath, "localPath")); + this.localPath = requireNonNull(localPath, "localPath"); this.remoteRepoUri = requireNonNull(remoteRepoUri, "remoteRepoUri"); - this.remotePath = normalizePath(requireNonNull(remotePath, "remotePath")); + this.remoteSubpath = remoteSubpath; this.remoteBranch = remoteBranch; this.gitignore = gitignore; @@ -81,7 +82,7 @@ protected AbstractMirror(Cron schedule, MirrorDirection direction, MirrorCredent // Use the properties' hash code so that the same properties result in the same jitter. jitterMillis = Math.abs(Objects.hash(this.schedule.asString(), this.direction, this.localRepo.parent().name(), this.localRepo.name(), - this.remoteRepoUri, this.remotePath, this.remoteBranch) / + this.remoteRepoUri, this.remoteSubpath, this.remoteBranch) / (Integer.MAX_VALUE / 60000)); } @@ -129,8 +130,8 @@ public final URI remoteRepoUri() { } @Override - public final String remotePath() { - return remotePath; + public String remoteSubpath() { + return remoteSubpath; } @Override @@ -180,7 +181,7 @@ public String toString() { .add("localRepo", localRepo.name()) .add("localPath", localPath) .add("remoteRepo", remoteRepoUri) - .add("remotePath", remotePath) + .add("remoteSubpath", remoteSubpath) .add("remoteBranch", remoteBranch) .add("gitignore", gitignore) .add("credential", credential); 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 1a75208268..8e0b0df093 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 @@ -16,11 +16,13 @@ package com.linecorp.centraldogma.server.internal.mirror; +import static com.linecorp.centraldogma.server.mirror.MirrorUtil.normalizePath; import static java.util.Objects.requireNonNull; import java.io.File; import java.net.URI; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.cronutils.model.Cron; @@ -37,16 +39,31 @@ public final class CentralDogmaMirror extends AbstractMirror { public CentralDogmaMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential, Repository localRepo, String localPath, - URI remoteRepoUri, String remoteProject, String remoteRepo, String remotePath, + URI remoteRepoUri, String remoteProject, String remoteRepo, String remoteSubpath, @Nullable String gitignore) { // Central Dogma has no notion of 'branch', so we just pass null as a placeholder. - super(schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, null, + super(schedule, + direction, + credential, + localRepo, + normalizePath(requireNonNull(localPath, "localPath")), + remoteRepoUri, + normalizePath(requireNonNull(remoteSubpath, "remoteSubpath")), + null, gitignore); this.remoteProject = requireNonNull(remoteProject, "remoteProject"); this.remoteRepo = requireNonNull(remoteRepo, "remoteRepo"); } + @Nonnull + @Override + public String remoteSubpath() { + final String remoteSubpath = super.remoteSubpath(); + assert remoteSubpath != null; + return remoteSubpath; + } + String remoteProject() { return remoteProject; } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirror.java index 6479bda959..aee8024b3f 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirror.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/GitMirror.java @@ -17,8 +17,10 @@ package com.linecorp.centraldogma.server.internal.mirror; import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_GIT_SSH; +import static com.linecorp.centraldogma.server.mirror.MirrorUtil.normalizePath; import static com.linecorp.centraldogma.server.storage.repository.FindOptions.FIND_ALL_WITHOUT_CONTENT; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; import java.io.ByteArrayInputStream; import java.io.File; @@ -33,6 +35,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.eclipse.jgit.api.FetchCommand; @@ -125,9 +128,16 @@ public final class GitMirror extends AbstractMirror { public GitMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential, Repository localRepo, String localPath, - URI remoteRepoUri, String remotePath, String remoteBranch, + URI remoteRepoUri, String remoteSubpath, String remoteBranch, @Nullable String gitignore) { - super(schedule, direction, credential, localRepo, localPath, remoteRepoUri, remotePath, remoteBranch, + super(schedule, + direction, + credential, + localRepo, + normalizePath(requireNonNull(localPath, "localPath")), + remoteRepoUri, + normalizePath(requireNonNull(remoteSubpath, "remoteSubpath")), + remoteBranch, gitignore); if (gitignore != null) { @@ -140,6 +150,14 @@ public GitMirror(Cron schedule, MirrorDirection direction, MirrorCredential cred } } + @Nonnull + @Override + public String remoteSubpath() { + final String remoteSubpath = super.remoteSubpath(); + assert remoteSubpath != null; + return remoteSubpath; + } + @Override protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { try (GitWithAuth git = openGit(workDir)) { @@ -155,7 +173,7 @@ protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumByt final ObjectId headTreeId = revWalk.parseTree(headCommitId).getId(); treeWalk.reset(headTreeId); - final String mirrorStatePath = remotePath() + LOCAL_TO_REMOTE_MIRROR_STATE_FILE_NAME; + final String mirrorStatePath = remoteSubpath() + LOCAL_TO_REMOTE_MIRROR_STATE_FILE_NAME; final Revision localHead = localRepo().normalizeNow(Revision.HEAD); final Revision remoteCurrentRevision = remoteCurrentRevision(reader, treeWalk, mirrorStatePath); if (localHead.equals(remoteCurrentRevision)) { @@ -253,14 +271,14 @@ protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, final String path = '/' + treeWalk.getPathString(); if (ignoreNode != null && - path.startsWith(remotePath()) && - ignoreNode.isIgnored('/' + path.substring(remotePath().length()), + path.startsWith(remoteSubpath()) && + ignoreNode.isIgnored('/' + path.substring(remoteSubpath().length()), fileMode == FileMode.TREE) == MatchResult.IGNORED) { continue; } if (fileMode == FileMode.TREE) { - maybeEnterSubtree(treeWalk, remotePath(), path); + maybeEnterSubtree(treeWalk, remoteSubpath(), path); continue; } @@ -270,11 +288,11 @@ protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, } // Skip the entries that are not under the remote path. - if (!path.startsWith(remotePath())) { + if (!path.startsWith(remoteSubpath())) { continue; } - final String localPath = localPath() + path.substring(remotePath().length()); + final String localPath = localPath() + path.substring(remoteSubpath().length()); // Skip the entry whose path does not conform to CD's path rule. if (!Util.isValidFilePath(localPath)) { @@ -405,7 +423,7 @@ private Revision remoteCurrentRevision( // Recurse into a directory if necessary. if (fileMode == FileMode.TREE) { - if (remotePath().startsWith(path + '/')) { + if (remoteSubpath().startsWith(path + '/')) { treeWalk.enterSubtree(); } continue; @@ -496,7 +514,7 @@ private void addModifiedEntryToCache(Revision localHead, DirCache dirCache, Obje // Recurse into a directory if necessary. if (fileMode == FileMode.TREE) { - maybeEnterSubtree(treeWalk, remotePath(), remoteFilePath); + maybeEnterSubtree(treeWalk, remoteSubpath(), remoteFilePath); continue; } @@ -506,11 +524,11 @@ private void addModifiedEntryToCache(Revision localHead, DirCache dirCache, Obje } // Skip the entries that are not under the remote path. - if (!remoteFilePath.startsWith(remotePath())) { + if (!remoteFilePath.startsWith(remoteSubpath())) { continue; } - final String localFilePath = localPath() + remoteFilePath.substring(remotePath().length()); + final String localFilePath = localPath() + remoteFilePath.substring(remoteSubpath().length()); // Skip the entry whose path does not conform to CD's path rule. if (!Util.isValidFilePath(localFilePath)) { @@ -553,7 +571,7 @@ private void addModifiedEntryToCache(Revision localHead, DirCache dirCache, Obje return; } - final String convertedPath = remotePath().substring(1) + // Strip the leading '/' + final String convertedPath = remoteSubpath().substring(1) + // Strip the leading '/' entry.getKey().substring(localPath().length()); final long contentLength = applyPathEdit(dirCache, inserter, convertedPath, value, null); numBytes += contentLength; @@ -605,12 +623,12 @@ private static byte[] currentEntryContent(ObjectReader reader, TreeWalk treeWalk } private static void maybeEnterSubtree( - TreeWalk treeWalk, String remotePath, String path) throws IOException { + TreeWalk treeWalk, String remoteSubpath, String path) throws IOException { // Enter if the directory is under the remote path. // e.g. // path == /foo/bar - // remotePath == /foo/ - if (path.startsWith(remotePath)) { + // remoteSubpath == /foo/ + if (path.startsWith(remoteSubpath)) { treeWalk.enterSubtree(); return; } @@ -618,9 +636,9 @@ private static void maybeEnterSubtree( // Enter if the directory is equal to the remote path. // e.g. // path == /foo - // remotePath == /foo/ + // remoteSubpath == /foo/ final int pathLen = path.length() + 1; // Include the trailing '/'. - if (pathLen == remotePath.length() && remotePath.startsWith(path)) { + if (pathLen == remoteSubpath.length() && remoteSubpath.startsWith(path)) { treeWalk.enterSubtree(); return; } @@ -628,8 +646,8 @@ private static void maybeEnterSubtree( // Enter if the directory is the parent of the remote path. // e.g. // path == /foo - // remotePath == /foo/bar/ - if (pathLen < remotePath.length() && remotePath.startsWith(path + '/')) { + // remoteSubpath == /foo/bar/ + if (pathLen < remoteSubpath.length() && remoteSubpath.startsWith(path + '/')) { treeWalk.enterSubtree(); } } diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/HttpMirror.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/HttpMirror.java new file mode 100644 index 0000000000..ad1fda78b4 --- /dev/null +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/HttpMirror.java @@ -0,0 +1,86 @@ +/* + * 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. + */ +package com.linecorp.centraldogma.server.internal.mirror; + +import java.io.File; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.cronutils.model.Cron; + +import com.linecorp.armeria.client.BlockingWebClient; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.server.HttpStatusException; +import com.linecorp.centraldogma.common.Change; +import com.linecorp.centraldogma.common.Markup; +import com.linecorp.centraldogma.common.Revision; +import com.linecorp.centraldogma.server.command.Command; +import com.linecorp.centraldogma.server.command.CommandExecutor; +import com.linecorp.centraldogma.server.mirror.MirrorCredential; +import com.linecorp.centraldogma.server.mirror.MirrorDirection; +import com.linecorp.centraldogma.server.storage.repository.Repository; + +public final class HttpMirror extends AbstractMirror { + + private static final Logger logger = LoggerFactory.getLogger(HttpMirror.class); + + public HttpMirror(Cron schedule, MirrorDirection direction, MirrorCredential credential, + Repository localRepo, String localPath, URI remoteUri) { + super(schedule, direction, credential, localRepo, localPath, remoteUri, null, null, null); + } + + @Override + protected void mirrorLocalToRemote(File workDir, int maxNumFiles, long maxNumBytes) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected void mirrorRemoteToLocal(File workDir, CommandExecutor executor, int maxNumFiles, + long maxNumBytes) throws Exception { + final BlockingWebClient client = WebClient.builder() + .maxResponseLength(maxNumBytes) + .build() + .blocking(); + + final AggregatedHttpResponse res = client.get(remoteRepoUri().toASCIIString()); + if (!res.status().isSuccess()) { + throw HttpStatusException.of(res.status()); + } + + final MediaType contentType = res.contentType(); + final Charset charset; + if (contentType != null) { + charset = contentType.charset(StandardCharsets.UTF_8); + } else { + charset = StandardCharsets.UTF_8; + } + + final String content = res.content(charset); + final String summary = "Mirror " + remoteRepoUri() + " to the repository '" + localRepo().name() + '\''; + logger.info(summary); + + executor.execute(Command.push( + MIRROR_AUTHOR, localRepo().parent().name(), localRepo().name(), + Revision.HEAD, summary, "", Markup.PLAINTEXT, + Change.ofTextUpsert(localPath(), content))).join(); + } +} diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTask.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTask.java index 14fd9ad578..fe8c8c73ef 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTask.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/mirror/MirroringTask.java @@ -35,7 +35,7 @@ private static Iterable generateTags(Mirror mirror) { return ImmutableList.of( Tag.of("direction", mirror.direction().name()), Tag.of("remoteBranch", firstNonNull(mirror.remoteBranch(), "")), - Tag.of("remotePath", mirror.remotePath()), + Tag.of("remoteSubpath", firstNonNull(mirror.remoteSubpath(), "")), Tag.of("localRepo", mirror.localRepo().name()), Tag.of("localPath", mirror.localPath())); } 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 0ebec7f92b..171068008e 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 @@ -21,6 +21,8 @@ import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_GIT_HTTP; import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_GIT_HTTPS; import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_GIT_SSH; +import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_HTTP; +import static com.linecorp.centraldogma.server.mirror.MirrorSchemes.SCHEME_HTTPS; import static com.linecorp.centraldogma.server.mirror.MirrorUtil.DOGMA_PATH_PATTERN; import static com.linecorp.centraldogma.server.mirror.MirrorUtil.split; import static java.util.Objects.requireNonNull; @@ -38,6 +40,7 @@ import com.linecorp.centraldogma.server.command.CommandExecutor; import com.linecorp.centraldogma.server.internal.mirror.CentralDogmaMirror; import com.linecorp.centraldogma.server.internal.mirror.GitMirror; +import com.linecorp.centraldogma.server.internal.mirror.HttpMirror; import com.linecorp.centraldogma.server.storage.repository.Repository; /** @@ -101,6 +104,10 @@ static Mirror of(Cron schedule, MirrorDirection direction, MirrorCredential cred URI.create(components[0]), components[1], components[2], gitignore); } + case SCHEME_HTTP: + case SCHEME_HTTPS: { + return new HttpMirror(schedule, direction, credential, localRepo, localPath, remoteUri); + } } throw new IllegalArgumentException("unsupported scheme in remoteUri: " + remoteUri); @@ -139,17 +146,21 @@ static Mirror of(Cron schedule, MirrorDirection direction, MirrorCredential cred String localPath(); /** - * Returns the URI of the Git repository which will be mirrored from. + * Returns the URI of the remote repository which will be mirrored from. */ URI remoteRepoUri(); /** - * Returns the path of the Git repository where is supposed to be mirrored. + * Returns the sub-path inside the remote repository whose content will be mirrored. + * The entire remote repository will be mirrored if this property is {@code null}. + * A subtree of the remote repository will be mirrored otherwise. */ - String remotePath(); + @Nullable + String remoteSubpath(); /** - * Returns the name of the branch in the Git repository where is supposed to be mirrored. + * Returns the name of the branch in the remote repository whose content will be mirrored. + * The default branch will be selected if this property is {@code null}. */ @Nullable String remoteBranch(); diff --git a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorSchemes.java b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorSchemes.java index 82dacb204d..15bc8dd753 100644 --- a/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorSchemes.java +++ b/server/src/main/java/com/linecorp/centraldogma/server/mirror/MirrorSchemes.java @@ -50,5 +50,15 @@ public final class MirrorSchemes { */ public static final String SCHEME_GIT_FILE = "git+file"; + /** + * {@code "http"}. + */ + public static final String SCHEME_HTTP = "http"; + + /** + * {@code "http"}. + */ + public static final String SCHEME_HTTPS = "https"; + private MirrorSchemes() {} } diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorTest.java index bfb54e8ae1..2b19d555bb 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/mirror/MirrorTest.java @@ -146,7 +146,7 @@ void jitter() { private static T assertMirror(String remoteUri, Class mirrorType, String expectedRemoteRepoUri, - String expectedRemotePath, + String expectedRemoteSubpath, @Nullable String expectedRemoteBranch) { final Repository repository = mock(Repository.class); final Project project = mock(Project.class); @@ -156,7 +156,7 @@ private static T assertMirror(String remoteUri, Class mirr final T m = newMirror(remoteUri, EVERY_MINUTE, repository, mirrorType); assertThat(m.remoteRepoUri().toString()).isEqualTo(expectedRemoteRepoUri); - assertThat(m.remotePath()).isEqualTo(expectedRemotePath); + assertThat(m.remoteSubpath()).isEqualTo(expectedRemoteSubpath); assertThat(m.remoteBranch()).isEqualTo(expectedRemoteBranch); return m; } diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepositoryTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepositoryTest.java index 79344014a3..56ded1d0e7 100644 --- a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepositoryTest.java +++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/DefaultMetaRepositoryTest.java @@ -202,9 +202,9 @@ void testSingleTypeMirror() { assertThat(bar.remoteRepoUri().toString()).isEqualTo("git+ssh://bar.com/bar.git"); assertThat(qux.remoteRepoUri().toString()).isEqualTo("git+ssh://qux.net/qux.git"); - assertThat(foo.remotePath()).isEqualTo("/"); - assertThat(bar.remotePath()).isEqualTo("/some-path/"); - assertThat(qux.remotePath()).isEqualTo("/"); + assertThat(foo.remoteSubpath()).isEqualTo("/"); + assertThat(bar.remoteSubpath()).isEqualTo("/some-path/"); + assertThat(qux.remoteSubpath()).isEqualTo("/"); assertThat(foo.remoteBranch()).isEqualTo("master"); assertThat(bar.remoteBranch()).isEqualTo("master"); @@ -327,9 +327,9 @@ void testMultipleTypeMirror() { assertThat(qux.remoteRepoUri().toASCIIString()).isEqualTo("dogma://qux.net/origin/qux.dogma"); // Ensure the remotePaths are generated correctly. - assertThat(foo.remotePath()).isEqualTo("/"); - assertThat(bar.remotePath()).isEqualTo("/"); - assertThat(qux.remotePath()).isEqualTo("/some-path/"); + assertThat(foo.remoteSubpath()).isEqualTo("/"); + assertThat(bar.remoteSubpath()).isEqualTo("/"); + assertThat(qux.remoteSubpath()).isEqualTo("/some-path/"); // Ensure the remoteBranches are generated correctly. assertThat(foo.remoteBranch()).isEqualTo("develop-foo");