diff --git a/.github/workflows/publish-maven-package.yml b/.github/workflows/publish-maven-package.yml index cb7efc1..b533ee5 100644 --- a/.github/workflows/publish-maven-package.yml +++ b/.github/workflows/publish-maven-package.yml @@ -24,10 +24,6 @@ jobs: server-id: github # Value of the distributionManagement/repository/id field of the pom.xml settings-path: ${{ github.workspace }} # location for the settings.xml file - - name: Compile Groovy code and generate sources - run: mvn gplus:compile generate-sources package - - #Manually first (once) set up an orphaned branch: # git switch --orphan packages # git commit --allow-empty -m "Initial commit on packages branch" @@ -35,15 +31,21 @@ jobs: - name: Install Package to packages branch run: | - MAINJAR=$(ls -1 target/*.jar | grep -v sources\.jar) - SOURCEJAR=$(ls -1 target/*.jar | grep sources\.jar | grep -v test) - VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) - echo Main JAR: $MAINJAR - echo Source JAR: $SOURCEJAR - echo Version: $VERSION - mkdir -p repository - echo Installing JAR in repository directory - mvn install:install-file -DpomFile=pom.xml -DlocalRepositoryPath=repository/ -Dfile="$MAINJAR" -Dsources="$SOURCEJAR" -DgeneratePom=true -DcreateChecksum=true + mkdir localm2 + mkdir -p repository/com/eficode/devstack + + echo Installing parent pom file + mvn install:install-file -Dpackaing=pom -Dfile=pom.xml -DpomFile=pom.xml -Dmaven.repo.local=localm2/ -DcreateChecksum=true + + echo Compiling, Packaging and Installing Groovy 2.5 version of library to local m2 directory + mvn install -f pom-2.5.xml -Dmaven.repo.local=localm2/ -DcreateChecksum=true + + echo Compiling, Packaging and Installing Groovy 3.0 version of library to local m2 directory + mvn install -f pom-3.0.xml -Dmaven.repo.local=localm2/ -DcreateChecksum=true + + echo Copying the new JAR files to repository which will be added to git branch "packages" + rsync -avh --checksum localm2/com/eficode/devstack repository/com/eficode/devstack + git config user.name github-actions git config user.email github-actions@github.com diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 9ca2fe0..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 40a6498..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index f960bc1..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/pom-2.5.xml b/pom-2.5.xml new file mode 100644 index 0000000..17a8186 --- /dev/null +++ b/pom-2.5.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + DevStack for Groovy 2.5 + devstack + 1.1.0-SNAPSHOT-groovy-2.5 + jar + + + + com.eficode + devstack + 1.1.0-SNAPSHOT + pom.xml + + + + + + org.codehaus.groovy + groovy + 2.5.18 + + + org.spockframework + spock-core + 2.2-groovy-2.5 + test + + + + + + com.eficode.atlassian + bitbucketinstancemanager + 0.0.3-SNAPSHOT-groovy-2.5 + standalone + + + + + + + + 11 + 11 + + + \ No newline at end of file diff --git a/pom-3.0.xml b/pom-3.0.xml new file mode 100644 index 0000000..a9819ed --- /dev/null +++ b/pom-3.0.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + DevStack for Groovy 3 + devstack + 1.1.0-SNAPSHOT-groovy-3.0 + jar + + + + com.eficode + devstack + 1.1.0-SNAPSHOT + pom.xml + + + + + + org.codehaus.groovy + groovy + 3.0.11 + + + org.spockframework + spock-core + 2.2-groovy-3.0 + test + + + + + + com.eficode.atlassian + bitbucketinstancemanager + 0.0.3-SNAPSHOT-groovy-2.5 + standalone + + + + + + + + 11 + 11 + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index ff9251e..d7f32e0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,39 +6,34 @@ com.eficode devstack - 1.0.6-SNAPSHOT + 1.1.0-SNAPSHOT + + DevStack - Parent pom + A series of scripts for setting up common developer application suites + pom - - - github - Eficode Devstack - https://maven.pkg.github.com/eficode/devStack - - - + + + org.codehaus.groovy - groovy-all + groovy 3.0.11 - pom - - - org.spockframework - spock-core - 2.1-groovy-3.0 + compile + - de.gesellix - docker-client - 2022-05-24T07-36-00 + org.slf4j + slf4j-api + 2.0.1 org.slf4j slf4j-simple - 1.7.36 + 2.0.0 @@ -52,30 +47,107 @@ commons-io 2.11.0 + - com.konghq - unirest-java - 3.13.6 - standalone + com.eficode.atlassian + jirainstancemanager + 1.1.0-SNAPSHOT + - com.eficode.atlassian - jirainstancemanger - 1.0.1-SNAPSHOT + de.gesellix + docker-client + 2022-07-28T22-55-00 + + + + org.junit.jupiter + junit-jupiter-api + 5.9.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.9.0 + test + + + eficode-github-jiraManagerRest - https://github.com/eficode/JiraInstanceMangerRest/raw/packages/repository/ + https://github.com/eficode/JiraInstanceManagerRest/raw/packages/repository/ + + + eficode-github-BitbucketInstanceManagerRest + https://github.com/eficode/BitbucketInstanceManagerRest/raw/packages/repository/ + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.0 + + + package + + shade + + + + true + + + standalone + + + + org.codehaus.groovy:groovy + com.google.code.gson:gson + org.apache.httpcomponents + commons-* + + + + + + + + true + + + + + org.apache.maven.plugins maven-source-plugin @@ -100,8 +172,12 @@ addSources addTestSources + generateStubs compile + generateTestStubs compileTests + removeStubs + removeTestStubs @@ -125,9 +201,27 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + + **/*Spec.class + **/*Test.java + + true + + + 11 11 diff --git a/src/main/groovy/com/eficode/devstack/container/Container.groovy b/src/main/groovy/com/eficode/devstack/container/Container.groovy index 518f359..c9bdffc 100644 --- a/src/main/groovy/com/eficode/devstack/container/Container.groovy +++ b/src/main/groovy/com/eficode/devstack/container/Container.groovy @@ -1,12 +1,18 @@ package com.eficode.devstack.container import de.gesellix.docker.client.DockerClientImpl +import de.gesellix.docker.client.network.ManageNetworkClient import de.gesellix.docker.engine.DockerClientConfig import de.gesellix.docker.engine.DockerEnv import de.gesellix.docker.engine.EngineResponse import de.gesellix.docker.remote.api.ContainerInspectResponse +import de.gesellix.docker.remote.api.ContainerState +import de.gesellix.docker.remote.api.ContainerSummary +import de.gesellix.docker.remote.api.EndpointSettings import de.gesellix.docker.remote.api.IdResponse import de.gesellix.docker.remote.api.Mount +import de.gesellix.docker.remote.api.Network +import de.gesellix.docker.remote.api.NetworkCreateRequest import de.gesellix.docker.remote.api.core.ClientException import de.gesellix.docker.remote.api.core.Frame import de.gesellix.docker.remote.api.core.StreamCallback @@ -27,19 +33,22 @@ import java.time.Duration trait Container { - static Logger log = LoggerFactory.getLogger(Container.class) - static DockerClientImpl dockerClient = new DockerClientImpl() + Logger log = LoggerFactory.getLogger(this.class) + DockerClientImpl dockerClient = new DockerClientImpl() + ManageNetworkClient networkClient = dockerClient.getManageNetwork() as ManageNetworkClient abstract String containerName abstract String containerMainPort + String containerNetworkName = "bridge" + String defaultShell = "/bin/bash" String containerId ArrayList mounts = [] - void prepareBindMount(String sourceAbs, String target, boolean readOnly = true){ - assert !isCreated() : "Bind mounts cant be prepared for already created container" + void prepareBindMount(String sourceAbs, String target, boolean readOnly = true) { + assert !isCreated(): "Bind mounts cant be prepared for already created container" this.mounts.add( - new Mount().tap{m -> + new Mount().tap { m -> m.source = sourceAbs m.target = target m.readOnly = readOnly @@ -51,8 +60,12 @@ trait Container { + abstract String createContainer(ArrayList cmd, ArrayList entrypoint) + abstract String createContainer() + abstract boolean runOnFirstStartup() + /** * Replaced the default docker connection (local) with a remote, secure one @@ -67,6 +80,9 @@ trait Container { dockerEnv.setTlsVerify("1") dockerConfig.apply(dockerEnv) dockerClient = new DockerClientImpl(dockerConfig) + networkClient = dockerClient.getManageNetwork() as ManageNetworkClient + + return ping() } @@ -85,8 +101,8 @@ trait Container { boolean isCreated() { - ArrayList content = dockerClient.ps().content - ArrayList containerNames = content.collect { it.Names }.flatten() + ArrayList content = dockerClient.ps().content + ArrayList containerNames = content.collect { it.names }.flatten() return containerNames.find { it == "/" + self.containerName } != null } @@ -107,10 +123,10 @@ trait Container { log.info("\tResolving container ID for:" + self.containerName) - ArrayList content = dockerClient.ps().content + ArrayList containers = dockerClient.ps().content - Map container = content.find { it.Names.first() == "/" + self.containerName } - this.containerId = container?.Id + ContainerSummary matchingContainer = containers.find { it.names.first() == "/" + self.containerName } + this.containerId = matchingContainer.id log.info("\tGot:" + this.containerId) return containerId @@ -118,41 +134,105 @@ trait Container { boolean startContainer() { + + log.info("Preparing to start container: ${self.containerName} (${self.containerId})") + boolean firstStartup = hasNeverBeenStarted() + + + Network network = getNetwork(self.containerNetworkName) + + if (!network) { + log.debug("\tContainers network is missing,creating it now") + network = createBridgeNetwork(self.containerNetworkName) + } + + setContainerNetworks([network]) dockerClient.startContainer(self.containerId) + + if (firstStartup) { + + log.debug("\tThis is the first time container starts, running one time startup tasks") + assert runOnFirstStartup(): "Error running initial startup commands inside of the container" + log.trace("\t\tFinished running first time startup tasks") + } + + return isRunning() } - ContainerInspectResponse inspect() { - return dockerClient.inspectContainer(self.containerId).content + ContainerInspectResponse inspectContainer() { + return self.containerId ? dockerClient.inspectContainer(self.containerId).content : null } boolean isRunning() { - return inspect().state.running + return inspectContainer()?.state?.running } - String getIp(){ - inspect().networkSettings.ipAddress + + /** + * Returns true if the container has been created but never started + * @return + */ + boolean hasNeverBeenStarted() { + + ContainerState.Status status = inspectContainer()?.state?.status + + if (status == ContainerState.Status.Created) { + return true //Created but not started + } else if (status == null) { + return true //Not even created + } else { + return false + } + } - boolean stopAndRemoveContainer() { + ArrayList getIps() { + ContainerInspectResponse inspectResponse = inspectContainer() + ArrayList ips = inspectResponse.networkSettings.networks.values().ipAddress + + if (inspectResponse.networkSettings.ipAddress != null ) { + ips.add(inspectResponse.networkSettings.ipAddress) + ips.unique(true) + } + + + + return ips + } + + ContainerState.Status status() { + return inspectContainer().state.status + } + + boolean stopAndRemoveContainer(Integer timeoutS = 5) { + + log.info("Stopping and removing container") + log.info("\tContainer: ${self.containerName} (${self.containerId})") if (self.containerId) { - dockerClient.stop(self.containerId, 240000) - dockerClient.wait(self.containerId) + + dockerClient.stop(self.containerId, timeoutS) + if (self.status() == ContainerState.Status.Running) { + dockerClient.kill(self.containerId) + } dockerClient.rm(self.containerId) try { - inspect() + inspectContainer() } catch (ClientException ex) { if (ex.response.message == "Not Found") { + containerId = null return true + } else { + throw new InputMismatchException("Error stopping and removing container") } } return false - }else { + } else { return false } @@ -161,7 +241,7 @@ trait Container { boolean stopContainer() { log.info("Stopping container:" + self.containerId) - dockerClient.stop(self.containerId, 240000) + running ? dockerClient.stop(self.containerId, 240000) : "" if (running) { log.warn("\tFailed to stop container" + self.containerId) return false @@ -172,7 +252,7 @@ trait Container { } - static File createTar(ArrayList filePaths, String outputPath) { + File createTar(ArrayList filePaths, String outputPath) { log.info("Creating tar file:" + outputPath) @@ -226,7 +306,7 @@ trait Container { } - static ArrayList extractTar(File tarFile, String outputPath) { + ArrayList extractTar(File tarFile, String outputPath) { log.info("Extracting: " + tarFile.path + " (${tarFile.size() / 1024}kB)") @@ -259,6 +339,190 @@ trait Container { } + /** + * Creates a network of the type bridge, or returns an existing one if one with the same name exists + * @param networkName name of the network + * @return the created/existing network + */ + Network createBridgeNetwork(String networkName) { + + log.info("Creating network:" + networkName) + + + Network existingNetwork = getBridgeNetwork(networkName) + + if (existingNetwork) { + log.info("\tNetwork already exists (${existingNetwork.id}), returning that.") + return existingNetwork + } + + + NetworkCreateRequest createRequest = new NetworkCreateRequest(networkName, false, "bridge", null, null, null, null, null, [:], null) + + + String networkId = networkClient.createNetwork(createRequest).content.id + assert networkId: "Error creating network:" + networkName + log.info("\tCreated:" + networkId) + + Network newNetwork = networkClient.networks([filters: [id: [networkId]]])?.content?.first() + assert newNetwork: "Error finding newly created network $networkName with id: " + networkId + + return newNetwork + } + + + boolean removeNetwork(Network network) { + + + log.info("Removing network:" + network.name) + + networkClient.rmNetwork(network.id) + + return networkClient.networks([filters: [id: [network.id]]])?.content?.isEmpty() + + } + + /** + * Gets a bridge network based on name or id, note there might be multiple networks with the same name + * @param networkNameOrId + * @return null or one of the matching networks + */ + Network getBridgeNetwork(String networkNameOrId) { + + + Network network = networkClient.networks().content.find { (it.name == networkNameOrId || it.id == networkNameOrId) && it.driver == "bridge" } + + return network + } + + + /** + * Gets a network based on name or id, note there might be multiple networks with the same name + * @param networkNameOrId + * @return Network if found, null if not + */ + Network getNetwork(String networkNameOrId) { + + + Network network = networkClient.networks().content.find { it.name == networkNameOrId || it.id == networkNameOrId } + + return network + } + + boolean networkIsValid(Network network) { + return getNetwork(network.id) != null + } + + ArrayList getContainerNetworks() { + Map rawResponse = inspectContainer().networkSettings.networks + + ArrayList networks = [] + rawResponse.keySet().each {networkId -> + Network network = getNetwork(networkId) + + if (network != null) { + networks.add(network) + }else if (networkId) { + //Handle networks that the container is attached to but that have been deleted + Network deletedNetwork = new Network() + deletedNetwork.id = networkId + deletedNetwork.driver = "deleted" + networks.add(deletedNetwork) + } + } + + return networks + } + + ArrayList getContainerBridgeNetworks() { + + + return getContainerNetworks().findAll {it.driver == "bridge"} + + } + + /** + * Connect container to an existing network. + * Note: + * A container will by default already belong to a network, so this method might connect the container to a second. + * @param network + * @return true on success + */ + boolean connectContainerToNetwork(Network network) throws InputMismatchException{ + + + log.info("Connecting container $containerId to network:" + network.name) + + if (!networkIsValid(network)) { + throw new InputMismatchException("Error connecting container (${containerName}) to network: ${network.name} (${network.id}). Network is not valid") + } + + networkClient.connectNetwork(network.id, containerId) + log.trace("\tVerifying container was added to network") + + + if (containerNetworks.find { it.id == network.id } != null) { + log.info("\tContainer was successfully added to network") + return true + } + log.error("\tContainer failed to be added to network") + throw new InputMismatchException("Error connecting container (${containerName}) to network: ${network.name} (${network.id})") + + + } + + boolean disconnectContainerFromNetwork(Network network) { + + log.info("Disconnecting container $containerId from network:" + network.name) + + networkClient.disconnectNetwork(network.id, containerId) + log.trace("\tVerifying container was disconnected from network") + + if (!containerNetworks.find { it.id == network.id }) { + log.info("\tContainer was successfully disconnected from network") + return true + } + + return false + + + } + + /** + * Sets networks for the container, disconnecting the container from any networks not in the list + * @param newNetworks A list of the networks that the container should be connected to + * @return true on success + */ + boolean setContainerNetworks(ArrayList newNetworks) throws InputMismatchException, AssertionError{ + + log.info("Setting container networks") + log.info("\tBeginning by disconnecting any networks it should no longer be connected to") + ArrayList networks = containerNetworks + containerNetworks.each { connectedNetwork -> + + if (newNetworks.id.find { newNetworkId -> newNetworkId != connectedNetwork.id }) { + assert disconnectContainerFromNetwork(connectedNetwork): "Error disconnecting container (${containerName}) from network: ${connectedNetwork.name} (${connectedNetwork.id})" + log.info("\t\tDisconnected container from network:" + connectedNetwork.name) + } + + } + log.info("\tFinished disconnecting container from unwanted networks, now connecting to new networks") + + ArrayListconnectedNetworks = containerNetworks + newNetworks.each { wantedNetwork -> + + if (connectedNetworks.id.find { wantedNetwork.id }) { + log.info("\t\tContainer already connected to ${wantedNetwork.name} (${wantedNetwork.id})") + } else { + assert connectContainerToNetwork(wantedNetwork): "Error connecting container (${containerName}) to network: ${wantedNetwork.name} (${wantedNetwork.id})" + log.info("\t\tConnected container to network:" + wantedNetwork.name) + } + + } + + } + + /** * Copy files from a container * @param containerPath can be a file or a path (ending in /) @@ -319,14 +583,22 @@ trait Container { } - ContainerCallback callBack = new ContainerCallback() - EngineResponse response = dockerClient.exec(self.containerId, ["/bin/bash", "-c", command], callBack, Duration.ofSeconds(timeoutS)) - + EngineResponse response = dockerClient.exec(self.containerId, [self.defaultShell, "-c", command], callBack, Duration.ofSeconds(timeoutS)) return callBack.output } + + String extractDomainFromUrl(String url) { + String out = url.replaceFirst(/^https?:\/\//, "") //Remove protocol + out = out.replaceFirst(/:\d+\\/?.*/, "") //Remove Port and anything after + out = out.replaceFirst(/\/.*/, "") //Remove subdomain + return out + } + + + } \ No newline at end of file diff --git a/src/main/groovy/com/eficode/devstack/container/impl/AlpineContainer.groovy b/src/main/groovy/com/eficode/devstack/container/impl/AlpineContainer.groovy new file mode 100644 index 0000000..570c0df --- /dev/null +++ b/src/main/groovy/com/eficode/devstack/container/impl/AlpineContainer.groovy @@ -0,0 +1,77 @@ +package com.eficode.devstack.container.impl + +import com.eficode.devstack.container.Container +import de.gesellix.docker.client.EngineResponseContent +import de.gesellix.docker.remote.api.ContainerCreateRequest +import de.gesellix.docker.remote.api.HostConfig +import de.gesellix.docker.remote.api.PortBinding + +class AlpineContainer implements Container { + + String containerName = "Alpine" + String containerMainPort = null + String containerImage = "alpine" + String containerImageTag = "latest" + String defaultShell = "/bin/sh" + + + AlpineContainer() {} + + /** + * Setup a secure connection to a remote docker + * @param dockerHost ex: https://docker.domain.com:2376 + * @param dockerCertPath ex: src/test/resources/dockerCert + */ + AlpineContainer(String dockerHost, String dockerCertPath) { + assert setupSecureRemoteConnection(dockerHost, dockerCertPath): "Error setting up secure remote docker connection" + } + + /** + * Will create an Alpine Container that will sleep indefinitely + * @return + */ + String createSleepyContainer(){ + return createContainer(["sleep", "infinity"], []) + } + + + String createContainer(ArrayList cmd , ArrayList entrypoint ) { + + assert ping(): "Error connecting to docker engine" + + ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest().tap { c -> + + c.image = containerImage + ":" + containerImageTag + c.hostname = containerName + + + } + + if (cmd.size()) { + containerCreateRequest.cmd = cmd + } + + if (entrypoint.size()) { + containerCreateRequest.entrypoint = entrypoint + } + + EngineResponseContent response = dockerClient.createContainer(containerCreateRequest, containerName) + assert response.content.warnings.isEmpty(): "Error when creating $containerName container:" + response.content.warnings.join(",") + + containerId = response.content.id + return containerId + + } + + String createContainer() { + return createContainer([], []) + } + + + + boolean runOnFirstStartup() { + + return true + } + +} diff --git a/src/main/groovy/com/eficode/devstack/container/impl/BitbucketContainer.groovy b/src/main/groovy/com/eficode/devstack/container/impl/BitbucketContainer.groovy new file mode 100644 index 0000000..a89de97 --- /dev/null +++ b/src/main/groovy/com/eficode/devstack/container/impl/BitbucketContainer.groovy @@ -0,0 +1,86 @@ +package com.eficode.devstack.container.impl + +import com.eficode.devstack.container.Container +import de.gesellix.docker.client.EngineResponseContent +import de.gesellix.docker.remote.api.ContainerCreateRequest +import de.gesellix.docker.remote.api.HostConfig +import de.gesellix.docker.remote.api.PortBinding + +class BitbucketContainer implements Container{ + + String containerName = "Bitbucket" + String containerMainPort = "7990" + String containerImage = "atlassian/bitbucket" + String containerImageTag = "latest" + long jvmMaxRam = 4096 + + + /** + * Setup a secure connection to a remote docker + * @param dockerHost ex: https://docker.domain.com:2376 + * @param dockerCertPath ex: src/test/resources/dockerCert + */ + BitbucketContainer(String dockerHost, String dockerCertPath) { + assert setupSecureRemoteConnection(dockerHost, dockerCertPath) : "Error setting up secure remote docker connection" + } + + BitbucketContainer() {} + + + /** + * Creates the container + * @return returns container ID + */ + String createContainer() { + + containerId = createBbContainer(this.containerName) + return containerId + + } + + String createContainer(ArrayList cmd , ArrayList entrypoint ) { + + if (cmd || entrypoint) { + throw new InputMismatchException("cmd and entrypoint cant be supplied to ${BitbucketContainer.simpleName}") + } + + return createContainer() + + } + + String createBbContainer(String containerName = this.containerName, String imageName = containerImage, String imageTag = containerImageTag, long maxRamMB = jvmMaxRam, String mainPort = containerMainPort) { + + assert dockerClient.ping().content as String == "OK", "Error Connecting to docker service" + + + ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest().tap { c -> + + c.image = imageName + ":" + imageTag + c.env = ["JVM_MAXIMUM_MEMORY=" + maxRamMB.toString() + "m", "JVM_MINIMUM_MEMORY=" + ((maxRamMB / 2) as String) + "m"] + c.exposedPorts = [(mainPort + "/tcp"): [:]] + c.hostConfig = new HostConfig().tap { h -> h.portBindings = [(mainPort + "/tcp"): [new PortBinding("0.0.0.0", (mainPort.toString()))]] } + c.hostname = containerName + + } + + + EngineResponseContent response = dockerClient.createContainer(containerCreateRequest, containerName) + assert response.content.warnings.isEmpty(): "Error when creating $containerName container:" + response.content.warnings.join(",") + + containerId = response.content.id + return containerId + + + } + + + boolean runOnFirstStartup() { + log.debug("\tUpdating apt and installing dependencies") + assert runBashCommandInContainer("apt update; apt install -y htop nano inetutils-ping; echo status: \$?", 20).any {it.contains("status: 0")} + + return true + } + + + +} diff --git a/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy b/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy index a57f833..0d91524 100644 --- a/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy +++ b/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy @@ -4,12 +4,14 @@ import com.eficode.devstack.container.Container import de.gesellix.docker.client.EngineResponseContent import de.gesellix.docker.remote.api.ContainerCreateRequest import de.gesellix.docker.remote.api.HostConfig +import de.gesellix.docker.remote.api.NetworkingConfig import de.gesellix.docker.remote.api.PortBinding -class JsmContainer implements Container{ +class JsmContainer implements Container { String containerName = "JSM" String containerMainPort = "8080" + ArrayList customEnvVar = [] String containerImage = "atlassian/jira-servicemanagement" String containerImageTag = "latest" long jvmMaxRam = 6000 @@ -18,14 +20,18 @@ class JsmContainer implements Container{ /** * Setup a secure connection to a remote docker - * @param dockerHost ex: https://docker.domain.com:2376 + * @param dockerHost ex: https://docker.domain.com:2376 * @param dockerCertPath ex: src/test/resources/dockerCert */ JsmContainer(String dockerHost, String dockerCertPath) { - assert setupSecureRemoteConnection(dockerHost, dockerCertPath) : "Error setting up secure remote docker connection" + assert setupSecureRemoteConnection(dockerHost, dockerCertPath): "Error setting up secure remote docker connection" } + /** + * Creates the container + * @return returns container ID + */ String createContainer() { containerId = createJsmContainer(this.containerName) @@ -33,22 +39,32 @@ class JsmContainer implements Container{ } + String createContainer(ArrayList cmd , ArrayList entrypoint ) { + + if (cmd || entrypoint) { + throw new InputMismatchException("cmd and entrypoint cant be supplied to ${JsmContainer.simpleName}") + } + + return createContainer() + + } + String createJsmContainer(String jsmContainerName = containerName, String imageName = containerImage, String imageTag = containerImageTag, long jsmMaxRamMB = jvmMaxRam, String jsmPort = containerMainPort) { - assert ping(), "Error Connecting to docker engine" + assert ping(), "Error Connecting to docker engine" ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest().tap { c -> c.image = imageName + ":" + imageTag - c.env = ["JVM_MAXIMUM_MEMORY=" + jsmMaxRamMB.toString() + "m", "JVM_MINIMUM_MEMORY=" + ((jsmMaxRamMB / 2) as String) + "m"] + c.env = ["JVM_MAXIMUM_MEMORY=" + jsmMaxRamMB.toString() + "m", "JVM_MINIMUM_MEMORY=" + ((jsmMaxRamMB / 2) as String) + "m"] + customEnvVar c.exposedPorts = [(jsmPort + "/tcp"): [:]] - c.hostConfig = new HostConfig().tap { h -> h.portBindings = [(jsmPort+"/tcp"): [new PortBinding("0.0.0.0", (jsmPort))]] } + c.hostConfig = new HostConfig().tap { h -> h.portBindings = [(jsmPort + "/tcp"): [new PortBinding("0.0.0.0", (jsmPort))]] } + c.hostname = containerName.toLowerCase() } - //EngineResponseContent response = dockerClient.run(containerCreateRequest, jsmContainerName) EngineResponseContent response = dockerClient.createContainer(containerCreateRequest, jsmContainerName) assert response.content.warnings.isEmpty(): "Error when creating $jsmContainerName container:" + response.content.warnings.join(",") @@ -59,4 +75,36 @@ class JsmContainer implements Container{ } + + /** + * Set custom environmental variables. Must be set before creating container + * @param keyVar Ex: ["key=value", "PATH=/user/local/sbin"] + */ + void setCustomEnvVar(ArrayList keyVar) { + + assert hasNeverBeenStarted(): "Error, cant set custom enviromental variables after creating container" + + this.customEnvVar = keyVar + } + + + boolean runOnFirstStartup() { + + ArrayList initialOut = runBashCommandInContainer("echo END") + initialOut.remove { "END" } + if (initialOut) { + log.warn("StdOut contains unexpected output on initial startup:") + initialOut.each { log.warn("\t" + it) } + } + log.debug("\tCreating folders needed for running Spoc tests with ScriptRunner") + assert runBashCommandInContainer("mkdir /opt/atlassian/jira/surefire-reports ; chown jira:jira /opt/atlassian/jira/surefire-reports").empty + log.debug("\tUpdating apt and installing dependencies") + assert runBashCommandInContainer("apt update; apt install -y htop nano inetutils-ping; echo status: \$?", 20).any {it.contains("status: 0")} + + return true + + + } + + } diff --git a/src/main/groovy/com/eficode/devstack/container/impl/NginxContainer.groovy b/src/main/groovy/com/eficode/devstack/container/impl/NginxContainer.groovy index b134ce2..0249e20 100644 --- a/src/main/groovy/com/eficode/devstack/container/impl/NginxContainer.groovy +++ b/src/main/groovy/com/eficode/devstack/container/impl/NginxContainer.groovy @@ -31,6 +31,16 @@ class NginxContainer implements Container{ return containerId } + String createContainer(ArrayList cmd , ArrayList entrypoint ) { + + if (cmd || entrypoint) { + throw new InputMismatchException("cmd and entrypoint cant be supplied to ${NginxContainer.simpleName}") + } + + return createContainer() + + } + /** * Bind local dir to nginx default root dir /usr/share/nginx/html @@ -55,6 +65,7 @@ class NginxContainer implements Container{ h.portBindings = [(containerMainPort+"/tcp"): [new PortBinding("0.0.0.0", (containerMainPort))]] h.mounts = this.mounts } + c.hostname = containerName } @@ -69,4 +80,8 @@ class NginxContainer implements Container{ } + + boolean runOnFirstStartup() { + return true + } } diff --git a/src/main/groovy/com/eficode/devstack/deployment/Deployment.groovy b/src/main/groovy/com/eficode/devstack/deployment/Deployment.groovy index e1f9e27..8c5272c 100644 --- a/src/main/groovy/com/eficode/devstack/deployment/Deployment.groovy +++ b/src/main/groovy/com/eficode/devstack/deployment/Deployment.groovy @@ -6,9 +6,10 @@ import org.slf4j.LoggerFactory trait Deployment { - static Logger log = LoggerFactory.getLogger(Deployment.class) + static Logger log = LoggerFactory.getLogger(this.class) abstract ArrayList containers abstract String friendlyName + String deploymentNetworkName = "bridge" abstract boolean setupDeployment() @@ -17,8 +18,6 @@ trait Deployment { log.info("Setting up secure connection to docker engine") assert getContainers() != null && !getContainers().empty: "Deployment has no containers defined" - //assert getContainers().any{! it.ping()} : "Connection has already been established." - //assert getContainers().created.each {!it} : "Cant setup secure connection when containers have already been created in docker engine" getContainers().each { @@ -30,6 +29,7 @@ trait Deployment { boolean startDeployment() { + log.info("Starting deployment: " + this.friendlyName) this.containers.each { container -> @@ -42,11 +42,26 @@ trait Deployment { } + boolean stopAndRemoveDeployment() { + + log.info("Stopping and removing deployment: " + this.getFriendlyName()) + + + this.getContainers().each { container -> + log.debug("\tStopping container:" + container.containerName) + assert container.stopAndRemoveContainer(0): "Error stopping container:" + container.containerId + } + + log.info("\tFinished stopping deployment") + return true + + } + boolean stopDeployment() { log.info("Stopping deployment: " + this.getFriendlyName()) - getContainers().each { container -> + this.getContainers().each { container -> log.debug("\tStopping container:" + container.containerName) assert container.stopContainer(): "Error stopping container:" + container.containerId } diff --git a/src/main/groovy/com/eficode/devstack/deployment/impl/BitbucketH2Deployment.groovy b/src/main/groovy/com/eficode/devstack/deployment/impl/BitbucketH2Deployment.groovy new file mode 100644 index 0000000..3645260 --- /dev/null +++ b/src/main/groovy/com/eficode/devstack/deployment/impl/BitbucketH2Deployment.groovy @@ -0,0 +1,73 @@ +package com.eficode.devstack.deployment.impl + +import com.eficode.atlassian.bitbucketInstanceManager.BitbucketInstanceManagerRest +import com.eficode.devstack.container.Container +import com.eficode.devstack.container.impl.BitbucketContainer +import com.eficode.devstack.deployment.Deployment + +class BitbucketH2Deployment implements Deployment{ + + String friendlyName = "Bitbucket H2 Deployment" + BitbucketInstanceManagerRest bitbucketRest + ArrayList containers = [] + + String bitbucketLicense + String bitbucketBaseUrl + + BitbucketH2Deployment(String bitbucketBaseUrl) { + + this.bitbucketBaseUrl = bitbucketBaseUrl + this.bitbucketRest = new BitbucketInstanceManagerRest(bitbucketBaseUrl) + this.containers = [new BitbucketContainer()] + bitbucketContainer.containerName = bitbucketContainer.extractDomainFromUrl(bitbucketBaseUrl) + + } + + BitbucketContainer getBitbucketContainer() { + return containers.find {it instanceof BitbucketContainer} as BitbucketContainer + } + + String getBitbucketContainerId() { + return bitbucketContainer.id + } + + void setBitbucketLicence(File licenseFile) { + bitbucketLicense = licenseFile.text + } + + void setBitbucketLicence(String licenseText) { + bitbucketLicense = licenseText + } + + + boolean setupDeployment() { + + log.info("Setting up deployment:" + friendlyName) + + assert bitbucketLicense : "Error no Bitbucket License has been setup" + + bitbucketContainer.createContainer() + log.info("\tCreated Bitbucket container:" + bitbucketContainer.id) + + log.info("\tConfiguring container to join network:" + this.deploymentNetworkName) + bitbucketContainer.containerNetworkName = this.deploymentNetworkName + + assert bitbucketContainer.startContainer() : "Error starting Bitbucket container:" + bitbucketContainer.id + log.info("\tStarted Bitbucket container") + + + + log.info("\tSetting up local H2 database, License, Name etc") + assert bitbucketRest.setApplicationProperties(bitbucketLicense, "Bitbucket", bitbucketBaseUrl) : "Error setting up H2 database for Bitbucket" + log.info("\t\tApplication basics setup successfully") + + + log.info("\tBitbucket deployment finished, you should now be able to login") + log.info("\t\tUrl:" + bitbucketBaseUrl) + log.info("\t\tUsername:" + bitbucketRest.adminUsername) + log.info("\t\tPassword:" + bitbucketRest.adminPassword) + + return true + + } +} diff --git a/src/main/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2Deployment.groovy b/src/main/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2Deployment.groovy new file mode 100644 index 0000000..f6236f0 --- /dev/null +++ b/src/main/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2Deployment.groovy @@ -0,0 +1,190 @@ +package com.eficode.devstack.deployment.impl + +import com.eficode.atlassian.bitbucketInstanceManager.BitbucketInstanceManagerRest +import com.eficode.atlassian.jiraInstanceManager.JiraInstanceManagerRest +import com.eficode.devstack.container.Container +import com.eficode.devstack.container.impl.BitbucketContainer +import com.eficode.devstack.container.impl.JsmContainer +import com.eficode.devstack.deployment.Deployment + +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class JsmAndBitbucketH2Deployment implements Deployment{ + + String friendlyName = "JIRA and Bitbucket H2 Deployment" + String containerNetworkName = "jsm_and_bitbucket" + ArrayList subDeployments = [] + + Map jiraAppsToInstall = [:] + String jiraLicense + String jiraBaseUrl + JiraInstanceManagerRest jiraRest + + String bitbucketBaseUrl + BitbucketInstanceManagerRest bitbucketRest + String bitbucketLicense + + JsmAndBitbucketH2Deployment(String jiraBaseUrl, String bitbucketBaseUrl) { + + this.jiraBaseUrl = jiraBaseUrl + this.jiraRest = new JiraInstanceManagerRest(jiraBaseUrl) + + this.bitbucketBaseUrl = bitbucketBaseUrl + this.bitbucketRest = new BitbucketInstanceManagerRest(bitbucketBaseUrl) + + + this.subDeployments = [new JsmH2Deployment(jiraBaseUrl), new BitbucketH2Deployment(bitbucketBaseUrl)] + + + + + } + + ArrayListgetContainers() { + return [jsmContainer, bitbucketContainer] + } + + void setContainers(ArrayList containers) { + this.containers = containers + } + + JsmH2Deployment getJsmH2Deployment() { + return subDeployments.find{it instanceof JsmH2Deployment} as JsmH2Deployment + } + + JsmContainer getJsmContainer() { + return jsmH2Deployment.jsmContainer + } + + BitbucketH2Deployment getBitbucketH2Deployment() { + return subDeployments.find{it instanceof BitbucketH2Deployment} as BitbucketH2Deployment + } + + BitbucketContainer getBitbucketContainer() { + return bitbucketH2Deployment.bitbucketContainer + } + + void setJiraLicense(String licenseText) { + this.jiraLicense = licenseText + } + + void setBitbucketLicense(String licenseText) { + + this.bitbucketLicense = licenseText + } + + /** + * Install apps in to JIRA + * @param appsAndLicenses key = App url (from marketplace), value = license string (optional) + * @return true if no apps where installed, or apps where installed successfully + */ + /* + boolean installJiraApps(Map appsAndLicenses = jiraAppsToInstall) { + + if (appsAndLicenses) { + log.info("Installing ${appsAndLicenses.size()} app(s)") + appsAndLicenses.each {url, license -> + assert jiraRest.installApp(url, license) : "Error installing app:" + url + } + } + + return true + + } + + */ + + + + private class SetupDeploymentTask implements Callable { + + Deployment deployment + + SetupDeploymentTask(Deployment deployment) { + this.deployment = deployment + } + + @Override + Boolean call() throws Exception { + this.deployment.setupDeployment() + } + } + + boolean setupDeployment() { + + log.info("Setting up deployment:" + friendlyName) + + assert jiraLicense : "Error no Jira License has been setup" + assert bitbucketLicense : "Error no Bitbucket License has been setup" + + jsmH2Deployment.setJiraLicense(new File(jiraLicense)) + bitbucketH2Deployment.setBitbucketLicence(new File(bitbucketLicense)) + + jsmH2Deployment.deploymentNetworkName = this.containerNetworkName + bitbucketH2Deployment.deploymentNetworkName = this.containerNetworkName + + ExecutorService threadPool = Executors.newFixedThreadPool(2) + Future jsmFuture = threadPool.submit(new SetupDeploymentTask(jsmH2Deployment)) + Future bitbucketFuture = threadPool.submit(new SetupDeploymentTask(bitbucketH2Deployment)) + threadPool.shutdown() + + + while(!jsmFuture.done || !bitbucketFuture.done) { + log.info("Waiting for deployments to finish") + log.info("\tJSM Finished:" + jsmFuture.done) + log.info("\tBitbucket Finished:" + bitbucketFuture.done) + + if(bitbucketFuture.done) { + log.info("\tBitbucket deployment finished successfully:" + bitbucketFuture.get()) + } + + if(jsmFuture.done) { + log.info("\tJSM deployment finished successfully:" + jsmFuture.get()) + } + + sleep(5000) + } + if(bitbucketFuture.done) { + log.info("\tBitbucket deployment finished successfully:" + bitbucketFuture.get()) + } + + if(jsmFuture.done) { + log.info("\tJSM deployment finished successfully:" + jsmFuture.get()) + } + + return jsmFuture.get() && bitbucketFuture.get() + + } + + @Override + void setupSecureDockerConnection(String host, String certPath) { + + subDeployments.each {deployment -> + deployment.setupSecureDockerConnection(host, certPath) + } + } + + /** + * Install apps in to JIRA + * @param appsAndLicenses key = App url (from marketplace), value = license string (optional) + * @return true if no apps where installed, or apps where installed successfully + */ + boolean installApps(Map appsAndLicenses = jiraAppsToInstall ) { + + if (appsAndLicenses) { + log.info("Installing ${appsAndLicenses.size()} app(s)") + appsAndLicenses.each {url, license -> + assert jiraRest.installApp(url, license) : "Error installing app:" + url + } + } + + return true + + } + + + +} diff --git a/src/main/groovy/com/eficode/devstack/deployment/impl/JsmH2Deployment.groovy b/src/main/groovy/com/eficode/devstack/deployment/impl/JsmH2Deployment.groovy index 72142e0..e5c426d 100644 --- a/src/main/groovy/com/eficode/devstack/deployment/impl/JsmH2Deployment.groovy +++ b/src/main/groovy/com/eficode/devstack/deployment/impl/JsmH2Deployment.groovy @@ -1,6 +1,6 @@ package com.eficode.devstack.deployment.impl -import com.eficode.atlassian.jiraInstanceManger.JiraInstanceMangerRest +import com.eficode.atlassian.jiraInstanceManager.JiraInstanceManagerRest import com.eficode.devstack.container.Container import com.eficode.devstack.container.impl.JsmContainer import com.eficode.devstack.deployment.Deployment @@ -8,7 +8,7 @@ import com.eficode.devstack.deployment.Deployment class JsmH2Deployment implements Deployment{ String friendlyName = "JIRA H2 Deployment" - JiraInstanceMangerRest jiraRest + JiraInstanceManagerRest jiraRest ArrayList containers = [] Map appsToInstall = [:] String jiraLicense @@ -17,8 +17,9 @@ class JsmH2Deployment implements Deployment{ JsmH2Deployment(String jiraBaseUrl) { this.jiraBaseUrl = jiraBaseUrl - this.jiraRest = new JiraInstanceMangerRest(jiraBaseUrl) + this.jiraRest = new JiraInstanceManagerRest(jiraBaseUrl) this.containers = [new JsmContainer()] + getJsmContainer().containerName = jsmContainer.extractDomainFromUrl(jiraBaseUrl) } JsmContainer getJsmContainer() { @@ -76,13 +77,14 @@ class JsmH2Deployment implements Deployment{ jsmContainer.createContainer() log.info("\tCreated jsm container:" + jsmContainer.id) + + log.info("\tConfiguring container to join network:" + this.deploymentNetworkName) + jsmContainer.containerNetworkName = this.deploymentNetworkName + assert jsmContainer.startContainer() : "Error starting JSM container:" + jsmContainer.id log.info("\tStarted JSM container") - log.debug("\tCreating folders needed for running Spoc tests with ScriptRunner") - assert jsmContainer.runBashCommandInContainer("mkdir /opt/atlassian/jira/surefire-reports ; chown jira:jira /opt/atlassian/jira/surefire-reports").empty - log.debug("\tUpdating apt and installing dependencies") - jsmContainer.runBashCommandInContainer("apt update; apt install -y htop nano", 20) + log.info("\tSetting up local H2 database") assert jiraRest.setupH2Database() : "Error setting up H2 database for JSM" diff --git a/src/main/groovy/com/eficode/devstack/simplelogger.properties b/src/main/groovy/com/eficode/devstack/simplelogger.properties new file mode 100644 index 0000000..684f767 --- /dev/null +++ b/src/main/groovy/com/eficode/devstack/simplelogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=trace +org.slf4j.simpleLogger.log.de.gesellix.docker.engine=warn +org.slf4j.simpleLogger.log.de.gesellix.docker.client=warn diff --git a/src/test/groovy/com/eficode/devstack/DevStackSpec.groovy b/src/test/groovy/com/eficode/devstack/DevStackSpec.groovy new file mode 100644 index 0000000..c5ffa61 --- /dev/null +++ b/src/test/groovy/com/eficode/devstack/DevStackSpec.groovy @@ -0,0 +1,184 @@ +package com.eficode.devstack + +import com.eficode.devstack.deployment.impl.JsmH2DeploymentTest +import de.gesellix.docker.client.DockerClientImpl +import de.gesellix.docker.engine.DockerClientConfig +import de.gesellix.docker.engine.DockerEnv +import de.gesellix.docker.remote.api.ContainerSummary +import org.apache.commons.io.FileUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Shared +import spock.lang.Specification + +class DevStackSpec extends Specification{ + + @Shared + String dockerRemoteHost = "https://docker.domain.se:2376" + @Shared + String dockerCertPath = "resources/dockerCert" + + @Shared + DockerClientImpl dockerClient + + @Shared + ArrayList containerNames + @Shared + ArrayList containerPorts + + + @Shared + static Logger log = LoggerFactory.getLogger(this.class) + + + def setup() { + cleanupContainers() + } + + def cleanup() { + cleanupContainers() + } + + def cleanupSpec() { + cleanupContainers() + } + + + + boolean cleanupContainers() { + + + + DockerClientImpl dockerClient = resolveDockerClient() + log.info("Cleaning up containers") + + ArrayList containers = dockerClient.ps().content + + log.debug("\tThere are currenlty ${containers.size()} containers") + log.debug("\tWill remove any container named:" + containerNames.join(",")) + log.debug("\tWill remove any container bound to ports:" + containerPorts.join(",")) + containers.each {container-> + + boolean nameCollision = false + container.names.any {existingName -> + containerNames.any {unwantedName -> + if (existingName == "/"+ unwantedName) { + nameCollision = true + } + } + } + + + boolean portCollision = false + + container.ports.find { existingPort -> + containerPorts.each {unwantedPort -> + if (existingPort.publicPort == unwantedPort) { + portCollision = true + } + } + } + + if (nameCollision || portCollision) { + log.info("\tWill kill and remove container: ${container.names.join(",")} (${container.id})") + log.debug("\t\tContainer has matching name:" + nameCollision + " (${container.names.join(",")})") + log.debug("\t\tContainer has matching port:" + portCollision + " (${container.ports.publicPort.join(",")})") + + if (container.state == "running") { + dockerClient.kill(container.id) + } + dockerClient.rm(container.id) + log.info("Stopped and removed container: ${container?.names?.join(",")} (${container?.id})") + } + } + + + } + + + /** + def stopAndRemoveContainer(ArrayList containerNames) { + + + DockerClientImpl dockerClient = resolveDockerClient() + + ArrayList containers = dockerClient.ps().content + + containerNames.each {containerName -> + + ContainerSummary container = containers.find { it.Names.first() == "/" + containerName } + String id = container?.id + + if (id) { + if (container.state == "running") { + dockerClient.kill(id) + } + dockerClient.rm(id) + log.info("Stopped and removed container: ${container?.names?.join(",")} (${container?.id})") + } + } + + + } + */ + + DockerClientImpl resolveDockerClient() { + + + log.info("Getting Docker client") + + if (!dockerRemoteHost) { + log.info("\tNo remote host configured, returning local docker connection") + return new DockerClientImpl() + } + + File certDir = new File(dockerCertPath) + + if (!certDir.isDirectory()) { + log.info("\tNo valid Docker Cert Path given, returning local docker connection") + return new DockerClientImpl() + } + log.info("\tLooking for docker certs in:" + certDir.absolutePath) + ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) + log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) + + + if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { + log.info("\tFound Docker certs, returning Secure remote Docker connection") + try { + DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) + assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" + return dockerClient + } catch (ex) { + log.error("\tError setting up connection to remote Docker engine:" + ex.message) + log.info("\tReturning local Docker connection") + return new DockerClientImpl() + } + + } + + log.info("\tMissing Docker certs, returning local docker connection") + + return new DockerClientImpl() + + } + + + /** + * Replaced the default docker connection (local) with a remote, secure one + * @param host ex: "https://docker.domain.se:2376" + * @param certPath folder containing ca.pem, cert.pem, key.pem + */ + static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { + + DockerClientConfig dockerConfig = new DockerClientConfig(host) + DockerEnv dockerEnv = new DockerEnv(host) + dockerEnv.setCertPath(certPath) + dockerEnv.setTlsVerify("1") + dockerConfig.apply(dockerEnv) + + return new DockerClientImpl(dockerConfig) + + } + +} \ No newline at end of file diff --git a/src/test/groovy/com/eficode/devstack/container/ContainerTest.groovy b/src/test/groovy/com/eficode/devstack/container/ContainerTest.groovy index 5dc9692..5e54ada 100644 --- a/src/test/groovy/com/eficode/devstack/container/ContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/ContainerTest.groovy @@ -1,13 +1,12 @@ package com.eficode.devstack.container +import com.eficode.devstack.container.impl.AlpineContainer +import de.gesellix.docker.remote.api.Network import groovy.io.FileType import org.slf4j.Logger import org.slf4j.LoggerFactory import spock.lang.Specification -import java.nio.file.Files -import java.nio.file.Path - class ContainerTest extends Specification { static Logger log = LoggerFactory.getLogger(ContainerTest.class) @@ -21,9 +20,20 @@ class ContainerTest extends Specification { String containerName = "spoc" String containerMainPort = "666" - String createContainer() {} - } + String createContainer() { + return "" + } + String createContainer(ArrayList cmd, ArrayList entrypoint) { + + return "" + } + + @Override + boolean runOnFirstStartup() { + return true + } + } def testPing() { @@ -37,12 +47,161 @@ class ContainerTest extends Specification { } + + def testNetworking() { + + //TODO continue this + + setup: + String networkName = "spock-network" + log.info("Testing CRUD of networks") + + AlpineContainer alpine1 = new AlpineContainer(dockerHost, dockerCertPath) + alpine1.containerName = "spock-alpine1" + + if (alpine1.created) { + assert alpine1.stopAndRemoveContainer(0) + } + alpine1.createSleepyContainer() + + AlpineContainer alpine2 = new AlpineContainer(dockerHost, dockerCertPath) + alpine2.containerName = "spock-alpine2" + if (alpine2.created) { + assert alpine2.stopAndRemoveContainer(0) + } + alpine2.createSleepyContainer() + + + log.info("\tCreated SPOCK container:" + alpine1.id) + + Network spockNetwork = alpine1.getBridgeNetwork(networkName) + Network removedNetwork = alpine1.getBridgeNetwork(networkName + "-removed") + if (spockNetwork) { + + assert alpine1.removeNetwork(spockNetwork): "Error removing pre-existing SPOCK network" + log.info("\tRemoved pre-existing spoc-network") + } + + if (removedNetwork) { + assert alpine1.removeNetwork(removedNetwork): "Error removing pre-existing SPOCK network" + log.info("\tRemoved pre-existing spoc-network") + } + + + when: "Creating two new networks, and deleting one of them" + spockNetwork = alpine1.createBridgeNetwork(networkName) + log.info("\tCreated spock network: ${spockNetwork.name} (${spockNetwork.id})") + removedNetwork = alpine1.createBridgeNetwork(networkName + "-removed") + log.info("\tCreated spock network: ${removedNetwork.name} (${removedNetwork.id})") + alpine1.removeNetwork(removedNetwork) + log.info("\tRemoved spock network:" + removedNetwork?.id) + + + then: "Methods should be able to find the non-deleted network" + + spockNetwork != null + removedNetwork != null + + alpine1.getBridgeNetwork(networkName) + alpine1.getBridgeNetwork(spockNetwork.id) + alpine1.getBridgeNetwork(networkName).name == networkName + alpine1.getBridgeNetwork(networkName).id == spockNetwork.id + alpine1.getBridgeNetwork(networkName).containers.isEmpty() + alpine1.getBridgeNetwork(networkName).driver == "bridge" + log.info("\tCreation of networks was tested successfully") + + + alpine1.removeNetwork(removedNetwork) + alpine1.getNetwork(removedNetwork.id) == null + alpine1.getNetwork(removedNetwork.name) == null + log.info("\tRemoval of networks was tested successfully") + + + expect: + alpine1.getContainerBridgeNetworks().size() == 1 + alpine1.connectContainerToNetwork(spockNetwork) + alpine1.getContainerBridgeNetworks().size() == 2 + alpine1.getContainerBridgeNetworks().find { it.id == spockNetwork.id } + + when: + alpine1.connectContainerToNetwork(removedNetwork) //The API doesn't verfy if a network is valid when connecting, this is done when container starts + + then: + InputMismatchException ex = thrown(InputMismatchException) + ex.message.contains("Network is not valid") + + when: "Setting the container network to a single network" + alpine1.setContainerNetworks([spockNetwork]) + + then: "The container default network should be disconnected and replace by the new network" + alpine1.getContainerBridgeNetworks() == [spockNetwork] + alpine1.getContainerBridgeNetworks().size() == 1 + + + when: "Setting the container network to a removed network" + alpine1.setContainerNetworks([removedNetwork]) + + then: "An error should be thrown" + InputMismatchException ex2 = thrown(InputMismatchException) + ex2.message.contains("Network is not valid") + + + when: "Starting both containers in the same network" + alpine1.setContainerNetworks([spockNetwork]) + alpine2.setContainerNetworks([spockNetwork]) + alpine1.startContainer() + alpine2.startContainer() + + then: "They should both be able to ping each other using containerName and ip" + alpine1.runBashCommandInContainer("ping -c 1 " + alpine2.containerName).any {it.contains("0% packet loss")} + alpine2.runBashCommandInContainer("ping -c 1 " + alpine1.containerName).any {it.contains("0% packet loss")} + alpine1.runBashCommandInContainer("ping -c 1 " + alpine2.ips.first()).any {it.contains("0% packet loss")} + alpine2.runBashCommandInContainer("ping -c 1 " + alpine1.ips.first()).any {it.contains("0% packet loss")} + + + + } + + def "Test extractDomainFromUrl"() { + + setup: + String expectedOutput = "host.domain.com" + ArrayListtestPatterns = [ + "host.domain.com", + "host.domain.com", + "http://host.domain.com", + "https://host.domain.com", + "host.domain.com/", + "host.domain.com/", + "http://host.domain.com/", + "https://host.domain.com/", + "host.domain.com:8080", + "http://host.domain.com:8080", + "https://host.domain.com:8080", + "host.domain.com/subdomain", + "http://host.domain.com/subdomain", + "https://host.domain.com/subdomain", + "host.domain.com:8080/subdomain", + "http://host.domain.com:8080/subdomain", + "https://host.domain.com:8080/subdomain" + ] + + + expect: + testPatterns.each {url-> + assert new AlpineContainer().extractDomainFromUrl(url) == expectedOutput + } + + + } + def testCreateTar() { setup: File tarOutDir = File.createTempDir("tarOut") File tarSourceDir = File.createTempDir("tarSourceDir") File tarSourceSubDir = new File(tarSourceDir.path + "/subDir") + AlpineContainer alpineContainer = new AlpineContainer() assert tarSourceSubDir.mkdir() ArrayList tarSourceRootFiles = [] @@ -50,13 +209,13 @@ class ContainerTest extends Specification { (0..9).each { i -> - File newRootFile = new File(tarSourceDir.absolutePath + "/tarRootFile${i}.txt") + File newRootFile = new File(tarSourceDir.absolutePath + "/tarRootFile${i}.txt") newRootFile.createNewFile() newRootFile.write("SPOC content for root file index: $i") tarSourceRootFiles.add(newRootFile) - File newSubFile = new File(tarSourceSubDir.absolutePath + "/tarSubFile${i}.txt") + File newSubFile = new File(tarSourceSubDir.absolutePath + "/tarSubFile${i}.txt") newSubFile.createNewFile() newSubFile.write("SPOC content for sub file index: $i") tarSourceSubFiles.add(newSubFile) @@ -72,31 +231,30 @@ class ContainerTest extends Specification { log.info("\t\tTar sub files:" + tarSourceSubFiles.name.join(",")) when: - File tarFile = ContainerImpl.createTar([tarSourceDir.absolutePath],tarOutDir.absolutePath + "/tarFile.tar" ) + File tarFile = alpineContainer.createTar([tarSourceDir.absolutePath], tarOutDir.absolutePath + "/tarFile.tar") then: tarOutDir.exists() when: - ArrayList extractedFiles = ContainerImpl.extractTar(tarFile, tarOutDir.absolutePath + "/") + ArrayList extractedFiles = alpineContainer.extractTar(tarFile, tarOutDir.absolutePath + "/") ArrayList allSourceFiles = tarSourceRootFiles + tarSourceSubFiles then: - tarOutDir.eachFileRecurse(FileType.FILES) {extractedFile -> + tarOutDir.eachFileRecurse(FileType.FILES) { extractedFile -> - if (extractedFile.name != tarFile.name ) { - File matchingSourceFile = allSourceFiles.find {it.name == extractedFile.name} + if (extractedFile.name != tarFile.name) { + File matchingSourceFile = allSourceFiles.find { it.name == extractedFile.name } - assert matchingSourceFile : "Could not find matching source file, for file found in tar:" + extractedFile.name + assert matchingSourceFile: "Could not find matching source file, for file found in tar:" + extractedFile.name assert matchingSourceFile.text == extractedFile.text - assert matchingSourceFile.relativePath(tarSourceDir) == extractedFile.relativePath(tarOutDir) + assert matchingSourceFile.relativePath(tarSourceDir) == extractedFile.relativePath(tarOutDir) } } - cleanup: tarSourceDir.deleteDir() ?: log.error("Error deleting temp files:" + tarSourceDir.absolutePath) tarOutDir.deleteDir() ?: log.error("Error deleting temp files:" + tarOutDir.absolutePath) @@ -104,6 +262,4 @@ class ContainerTest extends Specification { } - - } diff --git a/src/test/groovy/com/eficode/devstack/container/impl/BitbucketContainerTest.groovy b/src/test/groovy/com/eficode/devstack/container/impl/BitbucketContainerTest.groovy new file mode 100644 index 0000000..688f119 --- /dev/null +++ b/src/test/groovy/com/eficode/devstack/container/impl/BitbucketContainerTest.groovy @@ -0,0 +1,134 @@ +package com.eficode.devstack.container.impl + +import com.eficode.atlassian.bitbucketInstanceManager.BitbucketInstanceManagerRest +import de.gesellix.docker.client.DockerClientImpl +import de.gesellix.docker.engine.DockerClientConfig +import de.gesellix.docker.engine.DockerEnv +import de.gesellix.docker.remote.api.ContainerInspectResponse +import de.gesellix.docker.remote.api.ContainerState +import org.apache.commons.io.FileUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Shared +import spock.lang.Specification + +class BitbucketContainerTest extends Specification { + + @Shared + static Logger log = LoggerFactory.getLogger(BitbucketContainerTest.class) + + @Shared + DockerClientImpl dockerClient + + + @Shared + String dockerRemoteHost = "https://docker.domain.se:2376" + @Shared + String dockerCertPath = "resources/dockerCert" + @Shared + String bitbucketBaseUrl = "http://bitbucket.domain.se:7990" + + + def setupSpec() { + dockerClient = resolveDockerClient() + dockerClient.stop("Bitbucket") + dockerClient.rm("Bitbucket") + } + + + + def "test setupContainer"() { + setup: + log.info("Testing setup of BB container using trait method") + BitbucketContainer bbc = new BitbucketContainer(dockerRemoteHost, dockerCertPath) + bbc.containerName = bbc.extractDomainFromUrl(bitbucketBaseUrl) + BitbucketInstanceManagerRest bbr = new BitbucketInstanceManagerRest(bitbucketBaseUrl) + + bbc.stopAndRemoveContainer() + + when: + String containerId = bbc.createContainer() + ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerId).content + + + then: + assert containerInspect.name == "/" + bbc.containerName : "BB was not given the expected name" + assert containerInspect.state.status == ContainerState.Status.Created : "BB Container status is of unexpected value" + assert containerInspect.state.running == false : "BB Container was started even though it should only have been created" + assert dockerClient.inspectImage(containerInspect.image).content.repoTags.find {it == "atlassian/bitbucket:latest"} : "BB container was created with incorrect Docker image" + assert containerInspect.hostConfig.portBindings.containsKey("7990/tcp") : "BB Container port binding was not setup correctly" + log.info("\tBB Container was setup correctly") + + expect: + bbc.startContainer() + bbr.setApplicationProperties(new File("resources/bitbucket/licenses/bitbucketLicense").text) + bbr.status == "RUNNING" + + + + } + + + + + DockerClientImpl resolveDockerClient() { + + if (this.dockerClient) { + return this.dockerClient + } + + log.info("Getting Docker client") + + if (!dockerRemoteHost) { + log.info("\tNo remote host configured, returning local docker connection") + return new DockerClientImpl() + } + + File certDir = new File(dockerCertPath) + + if (!certDir.isDirectory()) { + log.info("\tNo valid Docker Cert Path given, returning local docker connection") + return new DockerClientImpl() + } + log.info("\tLooking for docker certs in:" + certDir.absolutePath) + ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) + log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) + + + if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { + log.info("\tFound Docker certs, returning Secure remote Docker connection") + try { + DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) + assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" + return dockerClient + } catch (ex) { + log.error("\tError setting up connection to remote Docker engine:" + ex.message) + log.info("\tReturning local Docker connection") + return new DockerClientImpl() + } + + } + + log.info("\tMissing Docker certs, returning local docker connection") + + return new DockerClientImpl() + + } + + /** + * Replaced the default docker connection (local) with a remote, secure one + * @param host ex: "https://docker.domain.se:2376" + * @param certPath folder containing ca.pem, cert.pem, key.pem + */ + static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { + + DockerClientConfig dockerConfig = new DockerClientConfig(host) + DockerEnv dockerEnv = new DockerEnv(host) + dockerEnv.setCertPath(certPath) + dockerEnv.setTlsVerify("1") + dockerConfig.apply(dockerEnv) + + return new DockerClientImpl(dockerConfig) + + } +} diff --git a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy index ab7f398..1c3ff72 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -1,6 +1,6 @@ package com.eficode.devstack.container.impl - +import com.eficode.devstack.DevStackSpec import de.gesellix.docker.client.DockerClientImpl import de.gesellix.docker.engine.DockerClientConfig import de.gesellix.docker.engine.DockerEnv @@ -16,27 +16,26 @@ import spock.lang.Specification import java.nio.file.Files import java.nio.file.Path -class JsmContainerTest extends Specification { +class JsmContainerTest extends DevStackSpec { - @Shared - static Logger log = LoggerFactory.getLogger(JsmContainerTest.class) - @Shared - DockerClientImpl dockerClient + def setupSpec() { + dockerRemoteHost = "https://docker.domain.se:2376" + dockerCertPath = "resources/dockerCert" - @Shared - String dockerRemoteHost = "https://docker.domain.se:2376" - @Shared - String dockerCertPath = "resources/dockerCert" + dockerClient = resolveDockerClient() + log = LoggerFactory.getLogger(JsmContainerTest.class) - def setupSpec() { dockerClient = resolveDockerClient() - dockerClient.stop("JSM") - dockerClient.rm("JSM") + + containerNames = ["jira.domain.se", "JSM", "Spoc-JSM"] + containerPorts = [8080] } + + def "test isCreated"() { when: @@ -63,8 +62,6 @@ class JsmContainerTest extends Specification { !jsm.isCreated() log.info("\tisCreated now again returns false") - cleanup: - containerId ? dockerClient.rm(containerId) : "" } @@ -104,8 +101,6 @@ class JsmContainerTest extends Specification { assert containerInspect.hostConfig.portBindings.containsKey("8080/tcp") : "JSM Container port binding was not setup correctly" log.info("\tJSM Container was setup correctly") - cleanup: - containerId ? dockerClient.rm(containerId) : "" } @@ -130,8 +125,6 @@ class JsmContainerTest extends Specification { assert dockerClient.inspectImage(containerInspect.image).content.repoTags.find {it == "atlassian/jira-servicemanagement:4-ubuntu-jdk11"} : "JSM container was created with incorrect Docker image" log.info("\tJSM Container was setup correctly") - cleanup: - containerId ? dockerClient.rm(containerId) : "" } @@ -227,75 +220,13 @@ class JsmContainerTest extends Specification { assert hashOutput == [ fileHash + " " + containerDstDir + largestFile.name] : "Output from container is not formatted as expected" - then: - true cleanup: - jsm.stopAndRemoveContainer() ? log.info("\tDeleted JSM container") : log.info("\tError deleting JSM container") log.info("\tDeleting temp dir:" + tempDir.toString()) FileUtils.deleteDirectory(tempDir.toFile()) } - DockerClientImpl resolveDockerClient() { - - if (this.dockerClient) { - return this.dockerClient - } - - log.info("Getting Docker client") - - if (!dockerRemoteHost) { - log.info("\tNo remote host configured, returning local docker connection") - return new DockerClientImpl() - } - File certDir = new File(dockerCertPath) - - if (!certDir.isDirectory()) { - log.info("\tNo valid Docker Cert Path given, returning local docker connection") - return new DockerClientImpl() - } - log.info("\tLooking for docker certs in:" + certDir.absolutePath) - ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) - log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) - - - if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { - log.info("\tFound Docker certs, returning Secure remote Docker connection") - try { - DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) - assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" - return dockerClient - } catch (ex) { - log.error("\tError setting up connection to remote Docker engine:" + ex.message) - log.info("\tReturning local Docker connection") - return new DockerClientImpl() - } - - } - - log.info("\tMissing Docker certs, returning local docker connection") - - return new DockerClientImpl() - - } - - /** - * Replaced the default docker connection (local) with a remote, secure one - * @param host ex: "https://docker.domain.se:2376" - * @param certPath folder containing ca.pem, cert.pem, key.pem - */ - static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { - - DockerClientConfig dockerConfig = new DockerClientConfig(host) - DockerEnv dockerEnv = new DockerEnv(host) - dockerEnv.setCertPath(certPath) - dockerEnv.setTlsVerify("1") - dockerConfig.apply(dockerEnv) - - return new DockerClientImpl(dockerConfig) - - } } diff --git a/src/test/groovy/com/eficode/devstack/container/impl/NginxContainerTest.groovy b/src/test/groovy/com/eficode/devstack/container/impl/NginxContainerTest.groovy index 506f914..731bace 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/NginxContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/NginxContainerTest.groovy @@ -1,9 +1,12 @@ package com.eficode.devstack.container.impl +import com.eficode.devstack.DevStackSpec +import de.gesellix.docker.client.DockerClient import de.gesellix.docker.client.DockerClientImpl import de.gesellix.docker.engine.DockerClientConfig import de.gesellix.docker.engine.DockerEnv import de.gesellix.docker.remote.api.ContainerInspectResponse +import de.gesellix.docker.remote.api.ContainerSummary import kong.unirest.Unirest import org.apache.commons.io.FileUtils import org.apache.commons.io.filefilter.FileFileFilter @@ -13,57 +16,63 @@ import spock.lang.Shared import spock.lang.Specification -class NginxContainerTest extends Specification { +class NginxContainerTest extends DevStackSpec { - @Shared - static Logger log = LoggerFactory.getLogger(NginxContainerTest.class) @Shared - DockerClientImpl dockerClient + File localNginxRoot = new File("/") - @Shared - File localNginxRoot = new File("examples") @Shared - String dockerRemoteHost //= "https://docker.domain.se:2376" - @Shared - String dockerCertPath //= "resources/dockerCert" - @Shared - String nginxUrl = "http://localhost" + String nginxUrl = "http://docker.domain.se" def setupSpec() { + + dockerRemoteHost = "https://docker.domain.se:2376" + dockerCertPath = "resources/dockerCert" + dockerClient = resolveDockerClient() - dockerClient.stop("Nginx") - dockerClient.rm("Nginx") - assert localNginxRoot.listFiles().findAll{it.isFile()}.size() > 0 : "localNginxRoot must contain at least one file" + log = LoggerFactory.getLogger(this.class) + + containerNames = ["Nginx"] + containerPorts = [80] + + + } + + def "test default setup"() { setup: - NginxContainer nginxC = new NginxContainer() + + NginxContainer nginxC = new NginxContainer(dockerRemoteHost, dockerCertPath) + expect: nginxC.createContainer() nginxC.startContainer() - cleanup: - nginxC.stopAndRemoveContainer() + //cleanup: + //nginxC.stopAndRemoveContainer() } + def "test setting nginx root bind dir"() { setup: log.info("Testing setting nginx root dir") log.info("\tWill use dir:" + localNginxRoot.absolutePath) - NginxContainer nginxC = new NginxContainer() + NginxContainer nginxC = new NginxContainer(dockerRemoteHost, dockerCertPath) + //stopAndRemoveContainer([nginxC.containerName]) when: "bindHtmlRoot should add a mount to the mounts array" nginxC.bindHtmlRoot(localNginxRoot.absolutePath, true) @@ -72,91 +81,41 @@ class NginxContainerTest extends Specification { nginxC.mounts.size() == 1 when: "After creating the container, the inspect result should confirm the mount" - nginxC.createContainer() - nginxC.startContainer() + String containerId = nginxC.createContainer() + log.info("\tCreated container:" + containerId) + assert nginxC.startContainer() : "Error starting container" ContainerInspectResponse inspectResponse = dockerClient.inspectContainer(nginxC.id).getContent() log.info("\tContainer created") then: - inspectResponse.mounts.find {it.source == localNginxRoot.absolutePath} + inspectResponse.mounts.find { it.source == localNginxRoot.absolutePath } log.info("\tDocker API confirms mount was created") - expect: "Nginx should return the expected files" - log.info("\tConfirming Nginx returns files found in the bind directory") - localNginxRoot.listFiles().findAll {it.isFile()}.every {file -> - log.debug("\t"*2 + "Requesting file:" + file.name) - int status = Unirest.get(nginxUrl + "/" + file.name).asEmpty().status - log.debug("\t"*3 + "HTTP Status:" + status) - return status == 200 - } + /** + * Needs to be reworked for remote docker engines - Unirest.get(nginxUrl + "/MISSINGFILE").asEmpty().status == 404 - - - - - } - - - DockerClientImpl resolveDockerClient() { - if (this.dockerClient) { - return this.dockerClient - } + expect: "Nginx should return the expected files" + log.info("\tConfirming Nginx returns files found in the bind directory") + localNginxRoot.listFiles().findAll {it.isFile()}.every {file -> + log.debug("\t"*2 + "Requesting file:" + file.name) + int status = Unirest.get(nginxUrl + "/" + file.name).asEmpty().status + log.debug("\t"*3 + "HTTP Status:" + status) + return status == 200 + } - log.info("Getting Docker client") + * + */ - if (!dockerRemoteHost) { - log.info("\tNo remote host configured, returning local docker connection") - return new DockerClientImpl() - } - File certDir = new File(dockerCertPath) - - if (!certDir.isDirectory()) { - log.info("\tNo valid Docker Cert Path given, returning local docker connection") - return new DockerClientImpl() - } - log.info("\tLooking for docker certs in:" + certDir.absolutePath) - ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) - log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) - - - if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { - log.info("\tFound Docker certs, returning Secure remote Docker connection") - try { - DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) - assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" - return dockerClient - } catch (ex) { - log.error("\tError setting up connection to remote Docker engine:" + ex.message) - log.info("\tReturning local Docker connection") - return new DockerClientImpl() - } + Unirest.get(nginxUrl + "/MISSINGFILE").asEmpty().status == 404 - } - log.info("\tMissing Docker certs, returning local docker connection") + //cleanup: + //nginxC.stopAndRemoveContainer() - return new DockerClientImpl() } - /** - * Replaced the default docker connection (local) with a remote, secure one - * @param host ex: "https://docker.domain.se:2376" - * @param certPath folder containing ca.pem, cert.pem, key.pem - */ - static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { - - DockerClientConfig dockerConfig = new DockerClientConfig(host) - DockerEnv dockerEnv = new DockerEnv(host) - dockerEnv.setCertPath(certPath) - dockerEnv.setTlsVerify("1") - dockerConfig.apply(dockerEnv) - - return new DockerClientImpl(dockerConfig) - - } } \ No newline at end of file diff --git a/src/test/groovy/com/eficode/devstack/deployment/impl/BitbucketH2DeploymentTest.groovy b/src/test/groovy/com/eficode/devstack/deployment/impl/BitbucketH2DeploymentTest.groovy new file mode 100644 index 0000000..f40f33d --- /dev/null +++ b/src/test/groovy/com/eficode/devstack/deployment/impl/BitbucketH2DeploymentTest.groovy @@ -0,0 +1,119 @@ +package com.eficode.devstack.deployment.impl + +import de.gesellix.docker.client.DockerClientImpl +import de.gesellix.docker.engine.DockerClientConfig +import de.gesellix.docker.engine.DockerEnv +import org.apache.commons.io.FileUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Shared +import spock.lang.Specification + +class BitbucketH2DeploymentTest extends Specification{ + + @Shared + static Logger log = LoggerFactory.getLogger(BitbucketH2DeploymentTest.class) + + @Shared + DockerClientImpl dockerClient + + + @Shared + String dockerRemoteHost = "https://docker.domain.se:2376" + @Shared + String dockerCertPath = "resources/dockerCert" + @Shared + String bitbucketBaseUrl = "http://bitbucket.domain.se:7990" + + @Shared + File bitbucketLicenseFile = new File("resources/bitbucket/licenses/bitbucketLicense") + + def setupSpec() { + + + dockerClient = resolveDockerClient() + dockerClient.stop("Bitbucket") + dockerClient.rm("Bitbucket") + } + + + def "def setupDeployment"() { + + setup: + BitbucketH2Deployment bitbucketDep = new BitbucketH2Deployment(bitbucketBaseUrl) + bitbucketDep.setupSecureDockerConnection(dockerRemoteHost, dockerCertPath) + bitbucketDep.setBitbucketLicence(bitbucketLicenseFile) + bitbucketDep.stopAndRemoveDeployment() + + + when: + boolean setupSuccess = bitbucketDep.setupDeployment() + + then: + setupSuccess + + } + + + + DockerClientImpl resolveDockerClient() { + + if (this.dockerClient) { + return this.dockerClient + } + + log.info("Getting Docker client") + + if (!dockerRemoteHost) { + log.info("\tNo remote host configured, returning local docker connection") + return new DockerClientImpl() + } + + File certDir = new File(dockerCertPath) + + if (!certDir.isDirectory()) { + log.info("\tNo valid Docker Cert Path given, returning local docker connection") + return new DockerClientImpl() + } + log.info("\tLooking for docker certs in:" + certDir.absolutePath) + ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) + log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) + + + if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { + log.info("\tFound Docker certs, returning Secure remote Docker connection") + try { + DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) + assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" + return dockerClient + } catch (ex) { + log.error("\tError setting up connection to remote Docker engine:" + ex.message) + log.info("\tReturning local Docker connection") + return new DockerClientImpl() + } + + } + + log.info("\tMissing Docker certs, returning local docker connection") + + return new DockerClientImpl() + + } + + /** + * Replaced the default docker connection (local) with a remote, secure one + * @param host ex: "https://docker.domain.se:2376" + * @param certPath folder containing ca.pem, cert.pem, key.pem + */ + static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { + + DockerClientConfig dockerConfig = new DockerClientConfig(host) + DockerEnv dockerEnv = new DockerEnv(host) + dockerEnv.setCertPath(certPath) + dockerEnv.setTlsVerify("1") + dockerConfig.apply(dockerEnv) + + return new DockerClientImpl(dockerConfig) + + } +} diff --git a/src/test/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2DeploymentTest.groovy b/src/test/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2DeploymentTest.groovy new file mode 100644 index 0000000..e9fbc0a --- /dev/null +++ b/src/test/groovy/com/eficode/devstack/deployment/impl/JsmAndBitbucketH2DeploymentTest.groovy @@ -0,0 +1,160 @@ +package com.eficode.devstack.deployment.impl + +import com.eficode.devstack.container.impl.JsmContainer +import de.gesellix.docker.client.DockerClientImpl +import de.gesellix.docker.engine.DockerClientConfig +import de.gesellix.docker.engine.DockerEnv +import de.gesellix.docker.remote.api.ContainerSummary +import org.apache.commons.io.FileUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import spock.lang.Shared +import spock.lang.Specification + +class JsmAndBitbucketH2DeploymentTest extends Specification{ + + @Shared + String dockerRemoteHost = "https://docker.domain.se:2376" + @Shared + String dockerCertPath = "resources/dockerCert" + + @Shared + String jiraBaseUrl = "http://jira.domain.se:8080" + + @Shared + String bitbucketBaseUrl = "http://bitbucket.domain.se:7990" + + @Shared + String jiraDomain + + @Shared + String bitbucketDomain + + @Shared + static Logger log = LoggerFactory.getLogger(JsmH2DeploymentTest.class) + + @Shared + File bitbucketLicenseFile = new File("resources/bitbucket/licenses/bitbucketLicense") + + @Shared + File jsmLicenseFile = new File("resources/jira/licenses/jsm.license") + + + def setupSpec() { + + + assert jsmLicenseFile.text.length() > 10 : "Jira license file does not appear valid" + assert bitbucketLicenseFile.text.length() > 10 : "Bitbucket license file does not appear valid" + + JsmContainer jsmContainerPlaceholder = new JsmContainer() + jiraDomain = jsmContainerPlaceholder.extractDomainFromUrl(jiraBaseUrl) + bitbucketDomain = jsmContainerPlaceholder.extractDomainFromUrl(bitbucketBaseUrl) + } + + def "test setupDeployment"() { + + setup: + + stopAndRemoveContainer([jiraDomain, bitbucketDomain]) + + JsmAndBitbucketH2Deployment jsmAndBb = new JsmAndBitbucketH2Deployment(jiraBaseUrl, bitbucketBaseUrl) + jsmAndBb.setupSecureDockerConnection(dockerRemoteHost, dockerCertPath) + + jsmAndBb.bitbucketLicense = bitbucketLicenseFile + jsmAndBb.jiraLicense = jsmLicenseFile + + + expect: + jsmAndBb.setupDeployment() + jsmAndBb.bitbucketContainer.runBashCommandInContainer("ping -c 1 ${jiraDomain}").any {it.contains("0% packet loss")} + jsmAndBb.jsmContainer.runBashCommandInContainer("ping -c 1 ${bitbucketDomain}").any {it.contains("0% packet loss")} + + } + + + + + def stopAndRemoveContainer(ArrayList containerNames) { + + + DockerClientImpl dockerClient = resolveDockerClient() + + ArrayList containers = dockerClient.ps().content + + containerNames.each {containerName -> + + ContainerSummary container = containers.find { it.names.first() == "/" + containerName } + String id = container?.id + + if (id) { + if (container.state == "running") { + dockerClient.kill(id) + } + dockerClient.rm(id) + log.info("Stopped and removed container: ${container?.names?.join(",")} (${container?.id})") + } + } + + + } + + DockerClientImpl resolveDockerClient() { + + + log.info("Getting Docker client") + + if (!dockerRemoteHost) { + log.info("\tNo remote host configured, returning local docker connection") + return new DockerClientImpl() + } + + File certDir = new File(dockerCertPath) + + if (!certDir.isDirectory()) { + log.info("\tNo valid Docker Cert Path given, returning local docker connection") + return new DockerClientImpl() + } + log.info("\tLooking for docker certs in:" + certDir.absolutePath) + ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) + log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) + + + if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { + log.info("\tFound Docker certs, returning Secure remote Docker connection") + try { + DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) + assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" + return dockerClient + } catch (ex) { + log.error("\tError setting up connection to remote Docker engine:" + ex.message) + log.info("\tReturning local Docker connection") + return new DockerClientImpl() + } + + } + + log.info("\tMissing Docker certs, returning local docker connection") + + return new DockerClientImpl() + + } + + /** + * Replaced the default docker connection (local) with a remote, secure one + * @param host ex: "https://docker.domain.se:2376" + * @param certPath folder containing ca.pem, cert.pem, key.pem + */ + DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { + + DockerClientConfig dockerConfig = new DockerClientConfig(host) + DockerEnv dockerEnv = new DockerEnv(host) + dockerEnv.setCertPath(certPath) + dockerEnv.setTlsVerify("1") + dockerConfig.apply(dockerEnv) + + return new DockerClientImpl(dockerConfig) + + } + + +} diff --git a/src/test/groovy/com/eficode/devstack/deployment/impl/JsmH2DeploymentTest.groovy b/src/test/groovy/com/eficode/devstack/deployment/impl/JsmH2DeploymentTest.groovy index 94885e6..e15e1c5 100644 --- a/src/test/groovy/com/eficode/devstack/deployment/impl/JsmH2DeploymentTest.groovy +++ b/src/test/groovy/com/eficode/devstack/deployment/impl/JsmH2DeploymentTest.groovy @@ -1,5 +1,6 @@ package com.eficode.devstack.deployment.impl +import com.eficode.devstack.DevStackSpec import de.gesellix.docker.client.DockerClientImpl import de.gesellix.docker.engine.DockerClientConfig import de.gesellix.docker.engine.DockerEnv @@ -9,28 +10,29 @@ import org.slf4j.LoggerFactory import spock.lang.Shared import spock.lang.Specification -class JsmH2DeploymentTest extends Specification { +class JsmH2DeploymentTest extends DevStackSpec { - @Shared - String dockerRemoteHost = "https://docker.domain.se:2376" - @Shared - String dockerCertPath = "resources/dockerCert" - - @Shared - String jiraBaseUrl = "http://192.168.0.1:8080" - //String jiraBaseUrl = "http://localhost:8080" @Shared - DockerClientImpl dockerClient - - @Shared - static Logger log = LoggerFactory.getLogger(JsmH2DeploymentTest.class) + String jiraBaseUrl = "http://jira.domain.se:8080" @Shared File projectRoot = new File(".") def setupSpec() { + dockerRemoteHost = "https://docker.domain.se:2376" + dockerCertPath = "resources/dockerCert" + + dockerClient = resolveDockerClient() + + log = LoggerFactory.getLogger(JsmH2DeploymentTest.class) + + dockerClient = resolveDockerClient() + + containerNames = ["jira.domain.se"] + containerPorts = [8080] + dockerClient = resolveDockerClient() dockerClient.stop("JSM") @@ -39,9 +41,10 @@ class JsmH2DeploymentTest extends Specification { def "test setupDeployment"() { setup: + JsmH2Deployment jsmDep = new JsmH2Deployment(jiraBaseUrl) jsmDep.setupSecureDockerConnection(dockerRemoteHost, dockerCertPath) - //jsmDep.jsmContainer.containerImageTag = "4.22.2" + jsmDep.setJiraLicense(new File(projectRoot.path + "/resources/jira/licenses/jsm.license")) jsmDep.appsToInstall = [ "https://marketplace.atlassian.com/download/apps/6820/version/1005740" : new File(projectRoot.path + "/resources/jira/licenses/scriptrunnerForJira.license").text, @@ -60,64 +63,7 @@ class JsmH2DeploymentTest extends Specification { } - DockerClientImpl resolveDockerClient() { - if (this.dockerClient) { - return this.dockerClient - } - log.info("Getting Docker client") - if (!dockerRemoteHost) { - log.info("\tNo remote host configured, returning local docker connection") - return new DockerClientImpl() - } - - File certDir = new File(dockerCertPath) - - if (!certDir.isDirectory()) { - log.info("\tNo valid Docker Cert Path given, returning local docker connection") - return new DockerClientImpl() - } - log.info("\tLooking for docker certs in:" + certDir.absolutePath) - ArrayList pemFiles = FileUtils.listFiles(certDir, ["pem"] as String[], false) - log.debug("\t\tFound pem files:" + pemFiles.name.join(",")) - - - if (!pemFiles.empty && pemFiles.every { pemFile -> ["ca.pem", "cert.pem", "key.pem"].find { it == pemFile.name } }) { - log.info("\tFound Docker certs, returning Secure remote Docker connection") - try { - DockerClientImpl dockerClient = setupSecureRemoteConnection(dockerRemoteHost, dockerCertPath) - assert dockerClient.ping().content as String == "OK": "Error pinging remote Docker engine" - return dockerClient - } catch (ex) { - log.error("\tError setting up connection to remote Docker engine:" + ex.message) - log.info("\tReturning local Docker connection") - return new DockerClientImpl() - } - - } - - log.info("\tMissing Docker certs, returning local docker connection") - - return new DockerClientImpl() - - } - - /** - * Replaced the default docker connection (local) with a remote, secure one - * @param host ex: "https://docker.domain.se:2376" - * @param certPath folder containing ca.pem, cert.pem, key.pem - */ - static DockerClientImpl setupSecureRemoteConnection(String host, String certPath) { - - DockerClientConfig dockerConfig = new DockerClientConfig(host) - DockerEnv dockerEnv = new DockerEnv(host) - dockerEnv.setCertPath(certPath) - dockerEnv.setTlsVerify("1") - dockerConfig.apply(dockerEnv) - - return new DockerClientImpl(dockerConfig) - - } } diff --git a/src/test/groovy/simplelogger.properties b/src/test/groovy/simplelogger.properties index 684f767..ac425fa 100644 --- a/src/test/groovy/simplelogger.properties +++ b/src/test/groovy/simplelogger.properties @@ -1,3 +1,4 @@ org.slf4j.simpleLogger.defaultLogLevel=trace +#org.slf4j.simpleLogger.log.org.eclipse.jgit=DEBUG org.slf4j.simpleLogger.log.de.gesellix.docker.engine=warn org.slf4j.simpleLogger.log.de.gesellix.docker.client=warn