diff --git a/src/main/java/com/github/sparsick/testcontainers/gitserver/GitServerContainer.java b/src/main/java/com/github/sparsick/testcontainers/gitserver/GitServerContainer.java index daffb12..d6c297f 100644 --- a/src/main/java/com/github/sparsick/testcontainers/gitserver/GitServerContainer.java +++ b/src/main/java/com/github/sparsick/testcontainers/gitserver/GitServerContainer.java @@ -5,6 +5,7 @@ import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; import java.io.IOException; import java.net.URI; @@ -18,17 +19,17 @@ public class GitServerContainer extends GenericContainer { private static final String GIT_PASSWORD_KEY = "GIT_PASSWORD"; private static DockerImageName DEFAULT_DOCKER_IMAGE_NAME = DockerImageName.parse("rockstorm/git-server"); private String gitRepoName = "testRepo"; + private String pathToExistingRepo; private SshIdentity sshClientIdentity; private SshHostKey hostKey; /** - * * @param dockerImageName - name of the docker image */ public GitServerContainer(DockerImageName dockerImageName) { super(dockerImageName); dockerImageName.assertCompatibleWith(DEFAULT_DOCKER_IMAGE_NAME); - if ("2.38".compareTo(dockerImageName.getVersionPart()) <= 0 ) { + if ("2.38".compareTo(dockerImageName.getVersionPart()) <= 0) { waitingFor(Wait.forLogMessage(".*Container configuration completed.*", 1)).addExposedPorts(22); } else { withExposedPorts(22); @@ -38,8 +39,9 @@ public GitServerContainer(DockerImageName dockerImageName) { /** * Override the default git password. - * + *

* Default password is 12345 + * * @param password - git password * @return instance of the git server container */ @@ -51,7 +53,7 @@ public GitServerContainer withGitPassword(String password) { /** * Override the default git repository name. - * + *

* Default name is "testRepo" * * @param gitRepoName - name of the git repository that is created by default @@ -69,10 +71,10 @@ public GitServerContainer withGitRepo(String gitRepoName) { */ public GitServerContainer withSshKeyAuth() { try { - sshClientIdentity = new SshIdentity( - this.getClass().getClassLoader().getResourceAsStream("id_client").readAllBytes(), - this.getClass().getClassLoader().getResourceAsStream("id_client.pub").readAllBytes(), - new byte[0]); + sshClientIdentity = new SshIdentity( + this.getClass().getClassLoader().getResourceAsStream("id_client").readAllBytes(), + this.getClass().getClassLoader().getResourceAsStream("id_client.pub").readAllBytes(), + new byte[0]); withClasspathResourceMapping("id_client.pub", "/home/git/.ssh/authorized_keys", BindMode.READ_ONLY); withClasspathResourceMapping("sshd_config", "/etc/ssh/sshd_config", BindMode.READ_ONLY); @@ -84,6 +86,19 @@ public GitServerContainer withSshKeyAuth() { return this; } + /** + * Copy an existing git repository to the container. + *

+ * The git repository is copied to the container and the git repository is initialized as bare repository. + * + * @param pathtoExistingRepo - path to the existing git repository. The path is relative to the project root. + * @return instance of the git server container + */ + public GitServerContainer withCopyExistingGitRepoToContainer(String pathtoExistingRepo) { + this.pathToExistingRepo = pathtoExistingRepo; + return this; + } + /** * Return the SSH URI for git repo. * @@ -91,30 +106,46 @@ public GitServerContainer withSshKeyAuth() { */ public URI getGitRepoURIAsSSH() { - return URI.create("ssh://git@"+ getHost() + ":" + getMappedPort(22) + "/srv/git/" + gitRepoName + ".git"); + return URI.create("ssh://git@" + getHost() + ":" + getMappedPort(22) + "/srv/git/" + gitRepoName + ".git"); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { super.containerIsStarted(containerInfo); - try { - String gitRepoPath = String.format("/srv/git/%s.git", gitRepoName); - execInContainer("mkdir", "-p", gitRepoPath); - execInContainer("git", "init", "--bare", gitRepoPath); - execInContainer("chown", "-R", "git:git", "/srv"); + configureGitRepository(); + collectHostKeyInformation(); + } + private void collectHostKeyInformation() { + try { ExecResult result = execInContainer("cat", "/etc/ssh/ssh_host_ecdsa_key.pub"); String[] catResult = result.getStdout().split(" "); hostKey = new SshHostKey(getHost(), Base64.getDecoder().decode(catResult[1])); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Could not collect host key information", e); + } + } + private void configureGitRepository() { + try { + String gitRepoPath = String.format("/srv/git/%s.git", gitRepoName); + if (pathToExistingRepo != null) { + copyFileToContainer(MountableFile.forHostPath(pathToExistingRepo + "/.git"), gitRepoPath); + execInContainer("git", "config", "--bool", "core.bare", "true", gitRepoPath); + execInContainer("chown", "-R", "git:git", "/srv"); + } else { + execInContainer("mkdir", "-p", gitRepoPath); + execInContainer("git", "init", "--bare", gitRepoPath); + execInContainer("chown", "-R", "git:git", "/srv"); + } } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); + throw new RuntimeException("Configure Git repository failed",e); } } /** * Return the Git Password that was set with the method {@code withGitPassword}. - * + *

* If no password was set, the default "12345" is returned. * * @return the git password @@ -126,7 +157,7 @@ public String getGitPassword() { /** * Return the identity information for public key authentication. - * + *

* If {@code withSshKeyAuth} was not called, then it returns null. * * @return identity information for a public key authentication @@ -143,4 +174,5 @@ public SshIdentity getSshClientIdentity() { public SshHostKey getHostKey() { return hostKey; } + } diff --git a/src/test/java/com/github/sparsick/testcontainers/gitserver/GitServerContainerTest.java b/src/test/java/com/github/sparsick/testcontainers/gitserver/GitServerContainerTest.java index 0853b0b..40029f9 100644 --- a/src/test/java/com/github/sparsick/testcontainers/gitserver/GitServerContainerTest.java +++ b/src/test/java/com/github/sparsick/testcontainers/gitserver/GitServerContainerTest.java @@ -5,18 +5,24 @@ import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory; import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig; import org.eclipse.jgit.util.FS; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.testcontainers.shaded.org.apache.commons.io.FileUtils; import org.testcontainers.utility.DockerImageName; import java.io.File; +import java.io.IOException; import java.net.URI; import java.util.List; import java.util.Map; @@ -27,13 +33,17 @@ public class GitServerContainerTest { + private static final DockerImageName LATEST_GIT_SERVER_VERSION = GitServerVersions.V2_40.getDockerImageName(); @TempDir(cleanup = CleanupMode.NEVER) private File tempDir; + + + @Test void validDockerImageName() { assertThatNoException().isThrownBy(() -> - new GitServerContainer(DockerImageName.parse("rockstorm/git-server:2.38")) + new GitServerContainer(LATEST_GIT_SERVER_VERSION) ); } @@ -46,7 +56,7 @@ void invalidDockerImageName() { @Test void gitPasswordIsSet() { - var containerUnderTest = new GitServerContainer(DockerImageName.parse("rockstorm/git-server:2.38")).withGitPassword("password"); + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION).withGitPassword("password"); Map envMap = containerUnderTest.getEnvMap(); @@ -55,7 +65,7 @@ void gitPasswordIsSet() { @Test void getGitPassword() { - var containerUnderTest = new GitServerContainer(DockerImageName.parse("rockstorm/git-server:2.38")).withGitPassword("password"); + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION).withGitPassword("password"); String gitPassword = containerUnderTest.getGitPassword(); @@ -64,7 +74,7 @@ void getGitPassword() { @Test void exposedPortIs22() { - var containerUnderTest = new GitServerContainer(DockerImageName.parse("rockstorm/git-server:2.38")); + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION); List exposedPorts = containerUnderTest.getExposedPorts(); assertThat(exposedPorts).containsOnly(22); @@ -82,7 +92,7 @@ void containerStarted(GitServerVersions gitServer) { @Test void gitRepoURI() { - var containerUnderTest = new GitServerContainer(DockerImageName.parse("rockstorm/git-server:2.38")).withGitRepo("testRepoName"); + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION).withGitRepo("testRepoName"); containerUnderTest.start(); @@ -91,6 +101,52 @@ void gitRepoURI() { assertThat(gitRepoURI.toString()).isEqualTo("ssh://git@"+ containerUnderTest.getHost() + ":" + gitPort + "/srv/git/testRepoName.git"); } + @Test + void copyExistingGitRepo(@TempDir File sampleRepo) throws GitAPIException, IOException { + initSampleRepo(sampleRepo, "src/test/resources/sampleRepo/testFile"); + + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION) + .withCopyExistingGitRepoToContainer(sampleRepo.getAbsolutePath()); + + containerUnderTest.start(); + + URI gitRepoURI = containerUnderTest.getGitRepoURIAsSSH(); + + assertThatNoException().isThrownBy(() -> + Git.cloneRepository() + .setURI(gitRepoURI.toString()) + .setDirectory(tempDir) + .setBranch("main") + .setTransportConfigCallback(GitServerContainerTest::configureWithPasswordAndNoHostKeyChecking) + .call() + ); + + assertThat(new File(tempDir, "testFile")).exists(); + } + + @Test + void copyExistingGitRepoWithCustomRepoName(@TempDir File sampleRepo) throws IOException, GitAPIException { + initSampleRepo(sampleRepo, "src/test/resources/sampleRepo/testFile"); + + var containerUnderTest = new GitServerContainer(LATEST_GIT_SERVER_VERSION) + .withGitRepo("customRepoName") + .withCopyExistingGitRepoToContainer(sampleRepo.getAbsolutePath()); + containerUnderTest.start(); + + URI gitRepoURI = containerUnderTest.getGitRepoURIAsSSH(); + + assertThatNoException().isThrownBy(() -> + Git.cloneRepository() + .setURI(gitRepoURI.toString()) + .setDirectory(tempDir) + .setBranch("main") + .setTransportConfigCallback(GitServerContainerTest::configureWithPasswordAndNoHostKeyChecking) + .call() + ); + + assertThat(new File(tempDir, "testFile")).exists(); + } + @ParameterizedTest @EnumSource(GitServerVersions.class) void setupGitRepo(GitServerVersions gitServer) { @@ -105,16 +161,7 @@ void setupGitRepo(GitServerVersions gitServer) { .setURI(gitRepoURI.toString()) .setDirectory(tempDir) .setBranch("main") - .setTransportConfigCallback(transport -> { - var sshTransport = (SshTransport) transport; - sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { - @Override - protected void configure(OpenSshConfig.Host hc, Session session) { - session.setPassword("12345"); - session.setConfig("StrictHostKeyChecking", "no"); - } - }); - }) + .setTransportConfigCallback(GitServerContainerTest::configureWithPasswordAndNoHostKeyChecking) .call() ); } @@ -133,26 +180,13 @@ void pubKeyAuth(GitServerVersions gitServer) { .setURI(gitRepoURI.toString()) .setDirectory(tempDir) .setBranch("main") - .setTransportConfigCallback(transport -> { - var sshTransport = (SshTransport) transport; - sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { - - @Override - protected JSch createDefaultJSch(FS fs ) throws JSchException { - JSch defaultJSch = super.createDefaultJSch( fs ); - configureSshIdentity(defaultJSch, containerUnderTest); - return defaultJSch; - } - @Override - protected void configure(OpenSshConfig.Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", "no"); - } - }); - }) + .setTransportConfigCallback(configureWithSshIdentityAndNoHostVerification(containerUnderTest.getSshClientIdentity())) .call() ); } + + @ParameterizedTest @EnumSource(GitServerVersions.class) void strictHostKeyVerifivation(GitServerVersions gitServer) { @@ -167,35 +201,78 @@ void strictHostKeyVerifivation(GitServerVersions gitServer) { .setURI(gitRepoURI.toString()) .setDirectory(tempDir) .setBranch("main") - .setTransportConfigCallback(transport -> { - var sshTransport = (SshTransport) transport; - sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { - - @Override - protected JSch createDefaultJSch(FS fs ) throws JSchException { - JSch defaultJSch = super.createDefaultJSch( fs ); - configureSshIdentity(defaultJSch, containerUnderTest); - configureHostKeyRepository(defaultJSch, containerUnderTest); - return defaultJSch; - } - }); - }) + .setTransportConfigCallback(configureWithSshIdentityAndHostKey(containerUnderTest.getSshClientIdentity(), containerUnderTest.getHostKey())) .call() ); } - private void configureSshIdentity(JSch defaultJSch, GitServerContainer containerUnderTest) throws JSchException { - SshIdentity sshIdentity = containerUnderTest.getSshClientIdentity(); + @NotNull + private TransportConfigCallback configureWithSshIdentityAndHostKey(SshIdentity sshIdentity, SshHostKey hostKey) { + return transport -> { + var sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { + + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + JSch defaultJSch = super.createDefaultJSch(fs); + configureSshIdentity(defaultJSch, sshIdentity); + configureHostKeyRepository(defaultJSch, hostKey); + return defaultJSch; + } + }); + }; + } + + private void configureSshIdentity(JSch defaultJSch, SshIdentity sshIdentity) throws JSchException { byte[] privateKey = sshIdentity.getPrivateKey(); byte[] publicKey = sshIdentity.getPublicKey(); byte[] passphrase = sshIdentity.getPassphrase(); defaultJSch.addIdentity("git-server", privateKey, publicKey, passphrase); } - private void configureHostKeyRepository(JSch defaultJSch, GitServerContainer containerUnderTest) throws JSchException { - SshHostKey hostKey = containerUnderTest.getHostKey(); + private void configureHostKeyRepository(JSch defaultJSch, SshHostKey hostKey) throws JSchException { String host = hostKey.getHostname(); byte[] key = hostKey.getKey(); defaultJSch.getHostKeyRepository().add(new HostKey(host, key), null); } + + private void initSampleRepo(File sampleRepo, String repoContent) throws IOException, GitAPIException { + FileUtils.copyFileToDirectory(new File(repoContent), sampleRepo); + + Git repo = Git.init().setDirectory(sampleRepo).setInitialBranch("main").call(); + repo.add().addFilepattern("testFile").call(); + repo.commit().setAuthor("Sandra Parsick", "sample@example.com").setMessage("init").call(); + } + + private static void configureWithPasswordAndNoHostKeyChecking(Transport transport) { + var sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setPassword("12345"); + session.setConfig("StrictHostKeyChecking", "no"); + } + }); + } + + @NotNull + private TransportConfigCallback configureWithSshIdentityAndNoHostVerification(SshIdentity sshIdentity) { + return transport -> { + var sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(new JschConfigSessionFactory() { + + @Override + protected JSch createDefaultJSch(FS fs) throws JSchException { + JSch defaultJSch = super.createDefaultJSch(fs); + configureSshIdentity(defaultJSch, sshIdentity); + return defaultJSch; + } + + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", "no"); + } + }); + }; + } } diff --git a/src/test/resources/sampleRepo/testFile b/src/test/resources/sampleRepo/testFile new file mode 100644 index 0000000..e69de29