diff --git a/README.md b/README.md index 6cf3b87..fa0a3f6 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ SubDeployments are simply a collection of deployments used by a more complex dep ## Utils These are classes mainly intended to be used by Container/Deployment-classes when massaging of the containers are needed for example. -Currently, [ImageBuilder.groovy](src%2Fmain%2Fgroovy%2Fcom%2Feficode%2Fdevstack%2Futil%2FImageBuilder.groovy) dynamically builds Atlassian images for non x86 architectures on the fly. +Currently, [ImageBuilder.groovy](src%2Fmain%2Fgroovy%2Fcom%2Feficode%2Fdevstack%2Futil%2FImageBuilder.groovy) dynamically builds Atlassian images on the fly. [TimeMachine.groovy](src%2Fmain%2Fgroovy%2Fcom%2Feficode%2Fdevstack%2Futil%2FTimeMachine.groovy) changes the apparent time for all containers sharing a Docker Engine, intended for testing date changes. @@ -155,4 +155,4 @@ mvn dependency:get -Dartifact=com.eficode:devstack-standalone:2.3.9-SNAPSHOT -Dr # Breaking Changes * 2.3.9 - * From now on two artifacts will be generated, devstack and devstack-standalone and the classifier standalone is deprecated \ No newline at end of file + * From now on two artifacts will be generated, devstack and devstack-standalone and the classifier standalone is deprecated diff --git a/pom.xml b/pom.xml index e744f84..d3b8f22 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.eficode devstack - 2.3.12-SNAPSHOT + 2.3.13-SNAPSHOT jar DevStack @@ -102,7 +102,7 @@ com.eficode.atlassian jirainstancemanager - 2.0.3-SNAPSHOT + 2.0.9-SNAPSHOT @@ -228,4 +228,4 @@ - \ No newline at end of file + diff --git a/src/main/groovy/com/eficode/devstack/container/Container.groovy b/src/main/groovy/com/eficode/devstack/container/Container.groovy index b99eb52..a61f068 100644 --- a/src/main/groovy/com/eficode/devstack/container/Container.groovy +++ b/src/main/groovy/com/eficode/devstack/container/Container.groovy @@ -974,4 +974,4 @@ trait Container { return callBack.output } -} \ No newline at end of file +} 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 0eb7714..5007718 100644 --- a/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy +++ b/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy @@ -23,6 +23,8 @@ class JsmContainer implements Container { long jvmMaxRam = 6000 private String debugPort //Contains the port used for JVM debug + private Boolean enableJvmTimeTravel //If true, jvm time travel will be enabled + JsmContainer(String dockerHost = "", String dockerCertPath = "") { if (dockerHost && dockerCertPath) { @@ -40,6 +42,10 @@ class JsmContainer implements Container { debugPort = portNr } + void enableJvmTimeTravel(boolean enable) { + this.enableJvmTimeTravel = true + } + /** * Gets the latest version number from Atlassian Marketplace * @return ex: 5.6.0 @@ -59,30 +65,33 @@ class JsmContainer implements Container { } @Override + //TODO check but looks like it always builds a custom image now, even if x86 ContainerCreateRequest setupContainerCreateRequest() { + log.debug("Setting up container create request for JSM container") - String image = containerImage + ":" + containerImageTag - log.debug("Setting up container create request for JSM container") - if (dockerClient.engineArch != "x86_64") { - log.debug("\tDocker engine is not x86, building custom JSM docker image") - - ImageBuilder imageBuilder = new ImageBuilder(dockerClient.host, dockerClient.certPath) - String jsmVersion = containerImageTag - if (jsmVersion == "latest") { - log.debug("\tCurrent image tag is set to \"latest\", need to resolve latest version number from Atlassian Marketplace in order to build custom image") - jsmVersion = getLatestJsmVersion() - } - log.debug("\tStarting building of Docker Image for JSM verion $jsmVersion") - ImageSummary newImage = imageBuilder.buildJsm(jsmVersion) - log.debug("\tFinished building custom image:" + newImage.repoTags.join(",")) + String jsmVersion = containerImageTag + if (jsmVersion == "latest") { + log.debug("\tCurrent image tag is set to \"latest\", need to resolve latest version number from Atlassian Marketplace in order to build custom image") + jsmVersion = getLatestJsmVersion() + } + log.debug("\tStarting building of Docker Image for JSM verion $jsmVersion") + ImageSummary jsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJsm(jsmVersion) + log.debug("\tFinished building custom image:" + jsmImage.repoTags.join(",")) + String imageNameAndTag = jsmImage.repoTags.first() - image = newImage.repoTags.first() + if (enableJvmTimeTravel) { + log.debug("\tStarting building of Docker Image for faketime JSM") + ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJvmFakeTime(jsmImage, false) + log.debug("\tFinished building custom image:" + faketimeJsmImage.repoTags.join(",")) + + imageNameAndTag = faketimeJsmImage.repoTags.first() } + ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest().tap { c -> - c.image = image + c.image = imageNameAndTag c.hostname = containerName c.env = ["JVM_MAXIMUM_MEMORY=" + jvmMaxRam + "m", "JVM_MINIMUM_MEMORY=" + ((jvmMaxRam / 2) as String) + "m", "ATL_TOMCAT_PORT=" + containerMainPort] + customEnvVar @@ -90,19 +99,24 @@ class JsmContainer implements Container { c.exposedPorts = [(containerMainPort + "/tcp"): [:]] c.hostConfig = new HostConfig().tap { h -> h.portBindings = [(containerMainPort + "/tcp"): [new PortBinding("0.0.0.0", (containerMainPort))]] - + ArrayList additionalJvmArgs = [] if (debugPort) { h.portBindings.put((debugPort + "/tcp"), [new PortBinding("0.0.0.0", (debugPort))]) c.exposedPorts.put((debugPort + "/tcp"), [:]) - c.env.add("JVM_SUPPORT_RECOMMENDED_ARGS=-Xdebug -Xrunjdwp:transport=dt_socket,address=*:${debugPort},server=y,suspend=n".toString()) + additionalJvmArgs += "-Xdebug -Xrunjdwp:transport=dt_socket,address=*:${debugPort},server=y,suspend=n".toString() } + if (enableJvmTimeTravel) { + additionalJvmArgs += "-XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_currentTimeMillis -XX:CompileCommand=dontinline,java.lang.System::currentTimeMillis -agentpath:/libfaketime.so".toString() + } + + + c.env.add("JVM_SUPPORT_RECOMMENDED_ARGS=${additionalJvmArgs.join(" ")}".toString()) h.mounts = this.preparedMounts } - } return containerCreateRequest @@ -115,7 +129,7 @@ class JsmContainer implements Container { * @return */ MountPoint getJiraHomeMountPoint() { - return getMounts().find {it.destination == "/var/atlassian/application-data/jira"} + return getMounts().find { it.destination == "/var/atlassian/application-data/jira" } } @@ -131,7 +145,7 @@ class JsmContainer implements Container { stopContainer() snapshotName = snapshotName ?: shortId + "-clone" - boolean success = dockerClient.overwriteVolume(snapshotName, jiraHomeMountPoint.name) + boolean success = dockerClient.overwriteVolume(snapshotName, jiraHomeMountPoint.name) if (wasRunning) { startContainer() } @@ -147,9 +161,9 @@ class JsmContainer implements Container { if (volumes.size() == 1) { return volumes.first() - }else if (volumes.isEmpty()) { + } else if (volumes.isEmpty()) { return null - }else { + } else { throw new InputMismatchException("Error finding snapshot volume:" + snapshotName) } @@ -171,7 +185,7 @@ class JsmContainer implements Container { snapshotName = snapshotName ?: shortId + "-clone" ArrayList existingVolumes = dockerClient.getVolumesWithName(snapshotName) - existingVolumes.each {existingVolume -> + existingVolumes.each { existingVolume -> log.debug("\tRemoving existing snapshot volume:" + existingVolume.name) dockerClient.manageVolume.rmVolume(existingVolume.name) } @@ -189,7 +203,7 @@ class JsmContainer implements Container { * Clone JIRA home volume * Container must be stopped * @param newVolumeName must be unique - * @param labels, optional labels to add to the new volume + * @param labels , optional labels to add to the new volume * @return */ Volume cloneJiraHome(String newVolumeName = "", Map labels = null) { @@ -197,8 +211,8 @@ class JsmContainer implements Container { newVolumeName = newVolumeName ?: shortId + "-clone" labels = labels ?: [ - srcContainerId : getId(), - created : System.currentTimeSeconds() + srcContainerId: getId(), + created : System.currentTimeSeconds() ] as Map Volume newVolume = dockerClient.cloneVolume(jiraHomeMountPoint.name, newVolumeName, labels) diff --git a/src/main/groovy/com/eficode/devstack/util/DockerClientDS.groovy b/src/main/groovy/com/eficode/devstack/util/DockerClientDS.groovy index 7f1048d..5cf33d6 100644 --- a/src/main/groovy/com/eficode/devstack/util/DockerClientDS.groovy +++ b/src/main/groovy/com/eficode/devstack/util/DockerClientDS.groovy @@ -6,6 +6,7 @@ import de.gesellix.docker.client.EngineResponseContent 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.ContainerSummary import de.gesellix.docker.remote.api.ExecConfig import de.gesellix.docker.remote.api.ExecStartConfig @@ -92,6 +93,21 @@ class DockerClientDS extends DockerClientImpl { } + ContainerSummary getContainerById(String completeId) { + EngineResponse response = ps(true, 1000, true, " {\"id\":[\"${completeId}\"]}") + + + ArrayList containers = response.content + + return containers.find{true} + + } + + + EngineResponseContent inspectContainer(ContainerSummary containerSummary){ + return inspectContainer(containerSummary.id) + } + EngineResponseContent createVolume(String name = null, Map labels = null, Map driverOpts = null) { VolumeCreateOptions volumeOptions = new VolumeCreateOptions() diff --git a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy index a978ac7..226cbf9 100644 --- a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy +++ b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy @@ -3,7 +3,6 @@ import com.eficode.devstack.container.impl.DoodContainer import de.gesellix.docker.remote.api.ImageSummary import java.util.concurrent.TimeoutException - /** * A utility class intended to build docker images so that they match the docker engines CPU architecture * @@ -16,7 +15,6 @@ class ImageBuilder extends DoodContainer { Map>builderOut = [:] long cmdTimeoutS = 800 //Will timeout individual container commands after this many seconds - ImageBuilder(String dockerHost, String dockerCertPath) { assert setupSecureRemoteConnection(dockerHost, dockerCertPath): "Error setting up secure remote docker connection" prepareBindMount("/var/run/docker.sock", "/var/run/docker.sock") @@ -48,11 +46,11 @@ class ImageBuilder extends DoodContainer { * @return */ ImageSummary buildJsm(String jsmVersion, boolean force = false){ - String imageName = "atlassian/jira-servicemanagement" String artifactName = "atlassian-servicedesk" String archType = dockerClient.engineArch - String imageTag = "$imageName:$jsmVersion-$archType" + String archTypeSuffix = archType == "x86_64" ? "" : "-$archType" + String imageTag = "$imageName:$jsmVersion$archTypeSuffix" containerName = imageTag.replaceAll(/[^a-zA-Z0-9_.-]/, "-").take(128-"-imageBuilder".length()) containerName += "-imageBuilder" @@ -76,6 +74,118 @@ class ImageBuilder extends DoodContainer { ImageSummary newImage = images.find {it.repoTags == [imageTag]} log.debug("\tFinished building image:" + imageTag + ", ID:" + newImage.id[7..17]) return newImage + } + + /* + ImageSummary buildFakeTimeJsm(String jsmVersion, boolean force = false){ + String imageName = "atlassian/jira-servicemanagement" + String artifactName = "atlassian-servicedesk" + String archType = dockerClient.engineArch + String archTypeSuffix = archType == "x86_64" ? "" : "-$archType" + String imageTag = "$imageName:$jsmVersion$archTypeSuffix" + String fakeTimeRoot = "/faketimebuild" + String fakeTimeDockerFilePath = "$fakeTimeRoot/Dockerfile" + String fakeTimeAgentFilePath = "$fakeTimeRoot/faketime.cpp" + String fakeTimeImageTag = "$imageName-faketime:$jsmVersion$archTypeSuffix" + String fakeTimCpp = getClass().getResourceAsStream("/faketime.cpp").text + containerName = fakeTimeImageTag.replaceAll(/[^a-zA-Z0-9_.-]/, "-").take(128-"-IB".length()) + containerName += "-IB" + + log.info("my name is now $containerName") + + //Check first if an image with the expected tag already exists + if (!force) { + ArrayList existingImages = dockerClient.images().content + ImageSummary existingImage = existingImages.find {it.repoTags == [fakeTimeImageTag]} + if (existingImage) { + return existingImage + } + } + + String fakeTimeDockerFile = """ + FROM $imageTag + WORKDIR / + RUN apt-get update && apt-get install -y wget g++ make + # RUN wget https://github.com/odnoklassniki/jvmti-tools/raw/master/faketime/faketime.cpp + COPY faketime.cpp . + RUN g++ -O2 -fPIC -shared -I \$JAVA_HOME/include -I \$JAVA_HOME/include/linux -olibfaketime.so faketime.cpp + + ENV JVM_SUPPORT_RECOMMENDED_ARGS="-agentpath:/libfaketime.so=+2592000000" + """ + + + putBuilderCommand("mkdir -p $fakeTimeRoot", "") + putBuilderCommand("cat > $fakeTimeDockerFilePath <<- 'EOF'\n" + fakeTimeDockerFile + "\nEOF", "") + putBuilderCommand("cat > $fakeTimeAgentFilePath <<- 'EOF'\n" + fakeTimCpp + "\nEOF", "") + putBuilderCommand("cd $fakeTimeRoot && docker build --tag $fakeTimeImageTag --build-arg JIRA_VERSION=$jsmVersion --build-arg ARTEFACT_NAME=$artifactName . && echo status:\$?", "status:0") + putBuilderCommand("pkill tail", "") + + assert build() : "Error building the image." + + ArrayList images = dockerClient.images().content + ImageSummary newImage = images.find {it.repoTags == [fakeTimeImageTag]} + return newImage + } + + */ + + + + ImageSummary buildJvmFakeTime(ImageSummary originalImage, boolean force = false) { + + String originalRepoTag = originalImage.repoTags.first() + String origImageName = originalRepoTag.substring(0,originalRepoTag.indexOf(":")) + String origImageTag = originalRepoTag.substring(originalRepoTag.indexOf(":")+ 1) + + return buildJvmFakeTime(origImageName, origImageTag, force) + + } + + //Presumes srcImage has "apt-get" commands + ImageSummary buildJvmFakeTime(String srcImage, String srcImageTag, boolean force) { + + String fakeTimeImageTag = "$srcImage-faketime:$srcImageTag" + containerName = fakeTimeImageTag.replaceAll(/[^a-zA-Z0-9_.-]/, "-").take(120-"-BuildFake".length()) + + String fakeTimeRoot = "/faketimebuild" + String fakeTimeDockerFilePath = "$fakeTimeRoot/Dockerfile" + String fakeTimeAgentFilePath = "$fakeTimeRoot/faketime.cpp" + String fakeTimCpp = getClass().getResourceAsStream("/faketime.cpp").text + + //Check first if an image with the expected tag already exists + if (!force) { + ArrayList existingImages = dockerClient.images().content + ImageSummary existingImage = existingImages.find {it.repoTags == [fakeTimeImageTag]} + if (existingImage) { + return existingImage + } + } + + String fakeTimeDockerFile = """ + FROM $srcImage:$srcImageTag + WORKDIR / + RUN apt-get update && apt-get install -y wget g++ make + COPY faketime.cpp /faketime.cpp + RUN g++ -O2 -fPIC -shared -I \$JAVA_HOME/include -I \$JAVA_HOME/include/linux -olibfaketime.so faketime.cpp + + """ + + // #ENV JVM_SUPPORT_RECOMMENDED_ARGS="-agentpath:/libfaketime.so=+2592000000" + //#RUN apt-get update && apt-get install -y g++ make + // #RUN wget https://github.com/odnoklassniki/jvmti-tools/raw/master/faketime/faketime.cpp + + putBuilderCommand("mkdir -p $fakeTimeRoot", "") + putBuilderCommand("cat > $fakeTimeDockerFilePath <<- 'EOF'\n" + fakeTimeDockerFile + "\nEOF", "") + putBuilderCommand("cat > $fakeTimeAgentFilePath <<- 'EOF'\n" + fakeTimCpp + "\nEOF", "") + putBuilderCommand("cd $fakeTimeRoot && docker build --tag $fakeTimeImageTag . && echo status:\$?", "status:0") + putBuilderCommand("pkill tail", "") + + + assert build() : "Error building the image." + + ArrayList images = dockerClient.images().content + ImageSummary newImage = images.find {it.repoTags == [fakeTimeImageTag]} + return newImage } @@ -140,7 +250,6 @@ class ImageBuilder extends DoodContainer { @Override boolean runAfterDockerSetup(){ - builderCommands.each {cmd, expectedLastOut -> log.info("Running container command:" + cmd) log.info("\tExpecting last output from command:" + expectedLastOut) diff --git a/src/main/resources/faketime.cpp b/src/main/resources/faketime.cpp new file mode 100644 index 0000000..585ba7c --- /dev/null +++ b/src/main/resources/faketime.cpp @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Odnoklassniki Ltd, Mail.Ru Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Original credit goes to: https://github.com/odnoklassniki/jvmti-tools/raw/master/faketime/faketime.cpp + */ + +#include +#include +#include +#include + +static jlong (*real_time_millis)(JNIEnv *, jclass) = NULL; +static jlong (*real_nano_time_adjustment)(JNIEnv *, jclass, jlong) = NULL; + +jlong JNICALL fake_time_millis(JNIEnv* env, jclass cls) +{ + jclass systemClass = env->FindClass("java/lang/System"); + jmethodID getPropertyMethodId = env->GetStaticMethodID(systemClass, "getProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + jstring offsetPropertyName = env->NewStringUTF("faketime.offset.seconds"); + jstring offsetPropertyDefault = env->NewStringUTF("0"); + jstring offsetValue = (jstring)env->CallStaticObjectMethod(systemClass, getPropertyMethodId, offsetPropertyName, offsetPropertyDefault); + const char *offset = env->GetStringUTFChars(offsetValue, NULL); + jlong result = real_time_millis(env, cls) + atoll(offset); + env->ReleaseStringUTFChars(offsetValue, offset); + return result; +} + +jlong JNICALL fake_nano_time_adjustment(JNIEnv *env, jclass cls, jlong offset_seconds) +{ + jclass systemClass = env->FindClass("java/lang/System"); + jmethodID getPropertyMethodId = env->GetStaticMethodID(systemClass, "getProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); + jstring offsetPropertyName = env->NewStringUTF("faketime.offset.seconds"); + jstring offsetPropertyDefault = env->NewStringUTF("0"); + jstring offsetValue = (jstring)env->CallStaticObjectMethod(systemClass, getPropertyMethodId, offsetPropertyName, offsetPropertyDefault); + const char *offset = env->GetStringUTFChars(offsetValue, NULL); + jlong result = real_nano_time_adjustment(env, cls, offset_seconds) + atoll(offset) * 1000000; + env->ReleaseStringUTFChars(offsetValue, offset); + return result; +} + +void JNICALL NativeMethodBind(jvmtiEnv *jvmti, JNIEnv *env, jthread thread, jmethodID method, + void *address, void **new_address_ptr) +{ + char *name; + if (jvmti->GetMethodName(method, &name, NULL, NULL) == 0) + { + if (real_time_millis == NULL && strcmp(name, "currentTimeMillis") == 0) + { + real_time_millis = (jlong(*)(JNIEnv *, jclass))address; + *new_address_ptr = (void *)fake_time_millis; + } + else if (real_nano_time_adjustment == NULL && strcmp(name, "getNanoTimeAdjustment") == 0) + { + real_nano_time_adjustment = (jlong(*)(JNIEnv *, jclass, jlong))address; + *new_address_ptr = (void *)fake_nano_time_adjustment; + } + jvmti->Deallocate((unsigned char *)name); + } +} + +JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) +{ + jvmtiEnv *jvmti; + vm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_0); + + jvmtiCapabilities capabilities = {0}; + capabilities.can_generate_native_method_bind_events = 1; +#if JNI_VERSION_9 + jvmtiCapabilities potential_capabilities; + jvmti->GetPotentialCapabilities(&potential_capabilities); + capabilities.can_generate_early_vmstart = potential_capabilities.can_generate_early_vmstart; +#endif + jvmti->AddCapabilities(&capabilities); + + jvmtiEventCallbacks callbacks = {0}; + callbacks.NativeMethodBind = NativeMethodBind; + jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); + + jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_NATIVE_METHOD_BIND, NULL); + + return 0; +} 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 ce5b2e9..eed52b5 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -3,6 +3,7 @@ package com.eficode.devstack.container.impl import com.eficode.devstack.DevStackSpec 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.core.ClientException import org.apache.commons.io.FileUtils import org.slf4j.LoggerFactory @@ -15,43 +16,42 @@ class JsmContainerTest extends DevStackSpec { def setupSpec() { - dockerRemoteHost = "https://docker.domain.se:2376" - dockerCertPath = "~/.docker/" + dockerRemoteHost = ""// "https://docker.domain.se:2376" + dockerCertPath = ""// "~/.docker/" - DevStackSpec.log = LoggerFactory.getLogger(JsmContainerTest.class) + log = LoggerFactory.getLogger(JsmContainerTest.class) cleanupContainerNames = ["jira.domain.se", "JSM", "Spoc-JSM"] cleanupContainerPorts = [8080] } - def "test isCreated"(String dockerHost, String certPath) { when: - DevStackSpec.log.info("Testing isCreated") + log.info("Testing isCreated") JsmContainer jsm = new JsmContainer(dockerHost, certPath) then: !jsm.isCreated() - DevStackSpec.log.info("\tDid not return a false positive") + log.info("\tDid not return a false positive") when: String containerId = jsm.createContainer() - DevStackSpec.log.info("\tCreated container:" + containerId) + log.info("\tCreated container:" + containerId) then: jsm.isCreated() - DevStackSpec.log.info("\tisCreated now returns true") + log.info("\tisCreated now returns true") when: - jsm.stopAndRemoveContainer() ?: {throw new Exception("Error revoming container $containerId")} - DevStackSpec.log.info("\tRemoved container") + jsm.stopAndRemoveContainer() ?: { throw new Exception("Error revoming container $containerId") } + log.info("\tRemoved container") then: !jsm.isCreated() - DevStackSpec.log.info("\tisCreated now again returns false") + log.info("\tisCreated now again returns false") where: dockerHost | certPath @@ -60,6 +60,7 @@ class JsmContainerTest extends DevStackSpec { } + def "test setupSecureRemoteConnection"() { /** @@ -70,31 +71,35 @@ class JsmContainerTest extends DevStackSpec { JsmContainer jsm = new JsmContainer(dockerRemoteHost, dockerCertPath) then: - assert jsm.ping(): "Error pinging docker engine" - assert jsm.dockerClient.dockerClientConfig.scheme == "https" + assert jsm.ping() || dockerRemoteHost == "": "Error pinging docker engine" + assert jsm.dockerClient.dockerClientConfig.scheme == "https" || dockerRemoteHost == "" } - def "test setupContainer"(String dockerHost, String certPath) { setup: - DevStackSpec.log.info("Testing setup of JSM container using trait method") + log.info("Testing setup of JSM container using trait method") JsmContainer jsm = new JsmContainer(dockerHost, certPath) + String archTypeSuffix = dockerClient.engineArch == "x86_64" ? "" : "-$dockerClient.engineArch" + String latestJsmVersion = JsmContainer.getLatestJsmVersion() + + //If arch != x86 a custom image will be built with a tag != latest + ArrayList expectedImageTags = ["atlassian/jira-servicemanagement:latest", "atlassian/jira-servicemanagement:$latestJsmVersion$archTypeSuffix".toString()] when: String containerId = jsm.createContainer() - ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerId).content + ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerId).content then: - assert containerInspect.name == "/" + jsm.containerName : "JSM was not given the expected name" - assert containerInspect.state.status == ContainerState.Status.Created : "JSM Container status is of unexpected value" - assert containerInspect.state.running == false : "JSM Container was started even though it should only have been created" - assert dockerClient.inspectImage(containerInspect.image).content.repoTags.find {it == "atlassian/jira-servicemanagement:latest"} : "JSM container was created with incorrect Docker image" - assert containerInspect.hostConfig.portBindings.containsKey("8080/tcp") : "JSM Container port binding was not setup correctly" - DevStackSpec.log.info("\tJSM Container was setup correctly") + assert containerInspect.name == "/" + jsm.containerName: "JSM was not given the expected name" + assert containerInspect.state.status == ContainerState.Status.Created: "JSM Container status is of unexpected value" + assert containerInspect.state.running == false: "JSM Container was started even though it should only have been created" + assert dockerClient.inspectImage(containerInspect.image).content.repoTags.any { it in expectedImageTags }: "JSM container was created with incorrect Docker image" + assert containerInspect.hostConfig.portBindings.containsKey("8080/tcp"): "JSM Container port binding was not setup correctly" + log.info("\tJSM Container was setup correctly") where: @@ -105,27 +110,24 @@ class JsmContainerTest extends DevStackSpec { } - def "test non standard parameters"(String dockerHost, String certPath) { setup: - DevStackSpec.log.info("Testing setup of JSM container using dedicated JSM method") + log.info("Testing setup of JSM container using dedicated JSM method") JsmContainer jsm = new JsmContainer(dockerHost, certPath) jsm.containerName = "Spoc-JSM" - jsm.containerImageTag = "4-ubuntu-jdk11" jsm.containerMainPort = "666" when: String containerId = jsm.createContainer() - ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerId).content + ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerId).content then: - assert containerInspect.name == "/Spoc-JSM" : "JSM was not given the expected name" - assert containerInspect.state.status == ContainerState.Status.Created : "JSM Container status is of unexpected value" - assert containerInspect.state.running == false : "JSM Container was started even though it should only have been created" - assert containerInspect.hostConfig.portBindings.containsKey("666/tcp") : "JSM Container port binding was not setup correctly" - assert dockerClient.inspectImage(containerInspect.image).content.repoTags.find {it == "atlassian/jira-servicemanagement:4-ubuntu-jdk11"} : "JSM container was created with incorrect Docker image" - DevStackSpec.log.info("\tJSM Container was setup correctly") + assert containerInspect.name == "/Spoc-JSM": "JSM was not given the expected name" + assert containerInspect.state.status == ContainerState.Status.Created: "JSM Container status is of unexpected value" + assert containerInspect.state.running == false: "JSM Container was started even though it should only have been created" + assert containerInspect.hostConfig.portBindings.containsKey("666/tcp"): "JSM Container port binding was not setup correctly" + log.info("\tJSM Container was setup correctly") where: @@ -136,16 +138,15 @@ class JsmContainerTest extends DevStackSpec { } - def "test stopAndRemoveContainer"(String dockerHost, String certPath) { setup: - DevStackSpec.log.info("Testing stop and removal of JSM container") + log.info("Testing stop and removal of JSM container") JsmContainer jsm = new JsmContainer(dockerHost, certPath) when: "Setting up the container with the trait method" - DevStackSpec.log.info("\tSetting up JSM container using trait method") + log.info("\tSetting up JSM container using trait method") String containerId = jsm.createContainer() then: "Removing it should return true" @@ -156,12 +157,11 @@ class JsmContainerTest extends DevStackSpec { then: "Exception should be thrown" ClientException ex = thrown(ClientException) - assert ex.message.startsWith("Client error : 404 Not Found") : "Unexpected exception thrown when inspecting the deleted container" - + assert ex.message.startsWith("Client error : 404 Not Found"): "Unexpected exception thrown when inspecting the deleted container" when: "Setting up the container with the trait method" - DevStackSpec.log.info("\tSetting up JSM container using dedicated JSM method") + log.info("\tSetting up JSM container using dedicated JSM method") String containerId2 = jsm.createContainer() then: "Removing it should return true" @@ -172,7 +172,7 @@ class JsmContainerTest extends DevStackSpec { then: "Exception should be thrown" ClientException ex2 = thrown(ClientException) - assert ex2.message.startsWith("Client error : 404 Not Found") : "Unexpected exception thrown when inspecting the deleted container" + assert ex2.message.startsWith("Client error : 404 Not Found"): "Unexpected exception thrown when inspecting the deleted container" where: dockerHost | certPath @@ -188,53 +188,52 @@ class JsmContainerTest extends DevStackSpec { String containerSrcPath = "/opt/atlassian/jira/atlassian-jira/WEB-INF/classes/com/atlassian/jira/" String containerDstDir = "/var/atlassian/application-data/jira/" - DevStackSpec.log.info("Testing copying files to and from JSM container") + log.info("Testing copying files to and from JSM container") JsmContainer jsm = new JsmContainer(dockerHost, certPath) String containerId = jsm.createContainer() - DevStackSpec.log.info("\tCreated container:" + containerId) + log.info("\tCreated container:" + containerId) Path tempDir = Files.createTempDirectory("testing-${this.class.simpleName}") - DevStackSpec.log.info("\tCreated temp dir:" + containerId.toString()) + log.info("\tCreated temp dir:" + containerId.toString()) when: "Copying files from container path:" - DevStackSpec.log.info("\tCopying files from container path:" + containerSrcPath) - ArrayListcopiedFiles = jsm.copyFilesFromContainer(containerSrcPath, tempDir.toString() + "/") - DevStackSpec.log.info("\tCopied ${copiedFiles.size()} files from container") + log.info("\tCopying files from container path:" + containerSrcPath) + ArrayList copiedFiles = jsm.copyFilesFromContainer(containerSrcPath, tempDir.toString() + "/") + log.info("\tCopied ${copiedFiles.size()} files from container") then: "Several files and directories should have been copied" - assert copiedFiles.size() : "No files where copied from container" - assert copiedFiles.any {it.directory} : "No directories where copied from container" - DevStackSpec.log.info("\tCopying files from container appears successful") + assert copiedFiles.size(): "No files where copied from container" + assert copiedFiles.any { it.directory }: "No directories where copied from container" + log.info("\tCopying files from container appears successful") when: "Copying a file to container" - assert jsm.startContainer() : "Error starting container" + assert jsm.startContainer(): "Error starting container" - File largestFile = copiedFiles.sort {it.size()}.last() + File largestFile = copiedFiles.sort { it.size() }.last() String fileHash = largestFile.bytes.sha256() - DevStackSpec.log.info("\tCopying file ($largestFile.name) to container path:" + containerDstDir + largestFile.name) - DevStackSpec.log.debug("\t\tFile size:" + (largestFile.size() * 0.000001).round(1) + "MB") - DevStackSpec.log.debug("\t\tFile hash:" + fileHash) + log.info("\tCopying file ($largestFile.name) to container path:" + containerDstDir + largestFile.name) + log.debug("\t\tFile size:" + (largestFile.size() * 0.000001).round(1) + "MB") + log.debug("\t\tFile hash:" + fileHash) then: "File should copy without error" jsm.copyFileToContainer(largestFile.path, containerDstDir) - DevStackSpec.log.info("\tFinished copying file to container") + log.info("\tFinished copying file to container") when: "Running a hash in the container" - DevStackSpec.log.info("Executing hash calculation of file in container") + log.info("Executing hash calculation of file in container") ArrayList hashOutput = jsm.runBashCommandInContainer("sha256sum " + containerDstDir + largestFile.name) - DevStackSpec.log.debug("\tContainer hash output:" + hashOutput) + log.debug("\tContainer hash output:" + hashOutput) then: "The container hash and local hash should be identical" - assert hashOutput.size() == 1 : "Expected one output row from remote bash command" - assert hashOutput.first().contains(fileHash) : "Output from container does not contain the expected hash" - assert hashOutput.first().contains(containerDstDir + largestFile.name) : "Output from container does not contain the expected file path" - assert hashOutput == [ fileHash + " " + containerDstDir + largestFile.name] : "Output from container is not formatted as expected" - + assert hashOutput.size() == 1: "Expected one output row from remote bash command" + assert hashOutput.first().contains(fileHash): "Output from container does not contain the expected hash" + assert hashOutput.first().contains(containerDstDir + largestFile.name): "Output from container does not contain the expected file path" + assert hashOutput == [fileHash + " " + containerDstDir + largestFile.name]: "Output from container is not formatted as expected" cleanup: - DevStackSpec.log.info("\tDeleting temp dir:" + tempDir.toString()) + log.info("\tDeleting temp dir:" + tempDir.toString()) FileUtils.deleteDirectory(tempDir.toFile()) @@ -246,6 +245,40 @@ class JsmContainerTest extends DevStackSpec { } + //Does not test functionality, just that the needed envs and bins seems to get deployed correctly + def "Test building of JSM JVM TimeTravel"(boolean enableTimeTravel, boolean enableJvmDebug) { + + + setup: + log.info("Testing setup and use of JVM TimeTravel enabled JSM container") + JsmContainer jsm = new JsmContainer() + + + when: "Setting up the container JvmTimeTravel enabled" + log.info("\tSetting up JSM container using trait method") + jsm.enableJvmTimeTravel(enableTimeTravel) + enableJvmDebug ? jsm.enableJvmDebug() : null + String containerId = jsm.createContainer() + ContainerSummary containerSummary = dockerClient.getContainerById(containerId) + ContainerInspectResponse containerInspect = dockerClient.inspectContainer(containerSummary).content + String jvmArgs = containerInspect.config.env.find { it.startsWith("JVM_SUPPORT_RECOMMENDED_ARGS") } + + jsm.startContainer() + + then: "The container should have envs and files enabling time travel" + assert !enableTimeTravel || jvmArgs.contains("-XX:DisableIntrinsic=_currentTimeMillis"): "Container is missing expected env var" + assert !enableTimeTravel || jvmArgs.contains("-XX:+UnlockDiagnosticVMOptions"): "Container is missing expected env var" + assert !enableTimeTravel || jvmArgs.contains("-agentpath:"): "Container is missing expected env var" + assert !enableJvmDebug || jvmArgs.contains("-Xdebug"): "JVM Debug was enabled but not is missing from env vars" + assert jsm.runBashCommandInContainer("test -f /faketime.cpp && echo status: \$?").contains("status: 0"): "Could not find the expected file /faketime.cpp in the container " + + + where: + enableTimeTravel | enableJvmDebug + true | false + true | true + false | false + } } 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 833bf26..7dd5a51 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.atlassian.jiraInstanceManager.beans.MarketplaceApp import com.eficode.devstack.DevStackSpec import kong.unirest.Unirest import org.slf4j.LoggerFactory @@ -8,8 +9,6 @@ import spock.lang.Shared class JsmH2DeploymentTest extends DevStackSpec { - - @Shared File projectRoot = new File(".") @@ -25,7 +24,7 @@ class JsmH2DeploymentTest extends DevStackSpec { cleanupContainerNames = ["jira.domain.se", "jira2.domain.se", "localhost"] cleanupContainerPorts = [8080, 8082, 80] - disableCleanup = false + disableCleanup = true } @@ -50,9 +49,8 @@ class JsmH2DeploymentTest extends DevStackSpec { jsmDep.jsmContainer.inspectContainer().networkSettings.ports.find { it.key == "$port/tcp" } //Make sure websudo was disabled - jsmDep.jsmContainer.runBashCommandInContainer("cat jira-config.properties").find {it == "jira.websudo.is.disabled=true"} - jsmDep.jsmContainer.containerLogs.find {it.matches(".*jira.websudo.is.disabled.*:.*true.*")} - + jsmDep.jsmContainer.runBashCommandInContainer("cat jira-config.properties").find { it == "jira.websudo.is.disabled=true" } + jsmDep.jsmContainer.containerLogs.find { it.matches(".*jira.websudo.is.disabled.*:.*true.*") } where: @@ -63,5 +61,82 @@ class JsmH2DeploymentTest extends DevStackSpec { } + def "test FakeTime"(String baseurl, String port, String dockerHost, String certPath) { + setup: + + JsmH2Deployment jsmDep = new JsmH2Deployment(baseurl, dockerHost, certPath) + + String srLicense = new File(System.getProperty("user.home") + "/.licenses/jira/sr.license").text + assert srLicense: "Error finding script runner license" + + + MarketplaceApp srMarketApp = MarketplaceApp.searchMarketplace("Adaptavist ScriptRunner for JIRA", MarketplaceApp.Hosting.Datacenter).find { it.key == "com.onresolve.jira.groovy.groovyrunner" } + MarketplaceApp.Version srVersion = srMarketApp?.getVersion("latest", MarketplaceApp.Hosting.Datacenter) + + jsmDep.setJiraLicense(new File(System.getProperty("user.home") + "/.licenses/jira/jsm.license").text) + jsmDep.appsToInstall.put(srVersion, srLicense) + jsmDep.jsmContainer.enableJvmTimeTravel(true) + when: + + + boolean setupSuccess = jsmDep.setupDeployment(true, true) + jsmDep.jiraRest.waitForSrToBeResponsive() + String jvmArgs = jsmDep.jsmContainer.inspectContainer().config.env.find { it.startsWith("JVM_SUPPORT_RECOMMENDED_ARGS") } + then: + assert setupSuccess: "Error setting up JIRA" + assert jvmArgs.contains("-XX:DisableIntrinsic=_currentTimeMillis"): "Container is missing expected env var" + assert jvmArgs.contains("-XX:+UnlockDiagnosticVMOptions"): "Container is missing expected env var" + assert jvmArgs.contains("-agentpath:"): "Container is missing expected env var" + assert jsmDep.jsmContainer.runBashCommandInContainer("test -f /faketime.cpp && echo status: \$?").contains("status: 0"): "Could not find the expected file /faketime.cpp in the container " + assert (new Date().toInstant().epochSecond - getJsmGroovyTime(jsmDep)).abs() < 5: "Time diff between JVM and localhost before any time traveling" + + + when: + log.info("Time traveling +60s") + assert setOffset(jsmDep, 600): "Error setting offset" + log.debug("\tSuccessfully set the time offset property") + sleep(30 * 1000) //Just to be sure the property change has time to get picked up + + then: + log.debug("\tVerifying the travel worked") + assert (getJsmGroovyTime(jsmDep) - new Date().toInstant().epochSecond) > 50: "Time diff between JVM and localhost before any time traveling" + log.debug("\tSuccessfully time traveled!") + + + where: + baseurl | port | dockerHost | certPath + //"http://localhost" | "80" | "" | "" + //"http://jira2.domain.se:8082" | "8082" | dockerRemoteHost | dockerCertPath + "http://jira.domain.se:8080" | "8080" | dockerRemoteHost | dockerCertPath + + } + + + long getJsmGroovyTime(JsmH2Deployment jsmDeploy) { + + + Map rawOut = jsmDeploy.jiraRest.executeLocalScriptFile("log.warn(\"EPOCH:\" + ( System.currentTimeMillis() / 1000).round(0))") + //Map rawOut = jsmDeploy.jiraRest.executeLocalScriptFile("log.warn(\"EPOCH:\" + new Date().toInstant().epochSecond)") + + assert rawOut.success == true: "There was an error querying for GroovyTime from JSM ScriptRunner" + assert (rawOut.log as ArrayList).size() == 1 + + String rawLogStatement = (rawOut.log as ArrayList).get(0) + long epochS = rawLogStatement.substring(rawLogStatement.lastIndexOf(":") + 1).toLong() + + return epochS + + } + + + boolean setOffset(JsmH2Deployment jsmDeploy, long offsetS) { + + Map rawOut = jsmDeploy.jiraRest.executeLocalScriptFile("System.setProperty(\"faketime.offset.seconds\", \"$offsetS\")") + + assert rawOut.success == true: "There was an error querying for GroovyTime from JSM ScriptRunner" + + return true + } + }