From fa6c16596f6e6ea1a1ca87c5f15ad895465f0a8a Mon Sep 17 00:00:00 2001 From: Joni Lahtinen Date: Tue, 21 Nov 2023 10:00:37 +0200 Subject: [PATCH 1/8] wip --- pom.xml | 4 +- .../devstack/container/Container.groovy | 2 +- .../container/impl/JsmContainer.groovy | 32 +++---- .../eficode/devstack/util/ImageBuilder.groovy | 52 ++++++++++- src/main/resources/faketime.cpp | 92 +++++++++++++++++++ 5 files changed, 158 insertions(+), 24 deletions(-) create mode 100644 src/main/resources/faketime.cpp diff --git a/pom.xml b/pom.xml index e744f84..fa0adfd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.eficode devstack - 2.3.12-SNAPSHOT + 2.3.13-SNAPSHOT jar DevStack @@ -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..8cfa564 100644 --- a/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy +++ b/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy @@ -60,25 +60,23 @@ class JsmContainer implements Container { @Override ContainerCreateRequest setupContainerCreateRequest() { - - 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(",")) - image = newImage.repoTags.first() + 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 jsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJsm(jsmVersion) + log.debug("\tFinished building custom image:" + jsmImage.repoTags.join(",")) + + log.debug("\tStarting building of Docker Image for faketime JSM verion $jsmVersion") + ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildFaketimeJsm(jsmVersion) + log.debug("\tFinished building custom image:" + faketimeJsmImage.repoTags.join(",")) + + String image = faketimeJsmImage.repoTags.first() ContainerCreateRequest containerCreateRequest = new ContainerCreateRequest().tap { c -> @@ -94,7 +92,7 @@ class JsmContainer implements Container { 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()) + c.env.add("JVM_SUPPORT_RECOMMENDED_ARGS=-XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_currentTimeMillis -XX:CompileCommand=dontinline,java.lang.System::currentTimeMillis -agentpath:/libfaketime.so=+2592000000 -Xdebug -Xrunjdwp:transport=dt_socket,address=*:${debugPort},server=y,suspend=n".toString()) } diff --git a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy index a978ac7..4bed7b5 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,7 +46,6 @@ 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 @@ -76,7 +73,55 @@ 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 imageTag = "$imageName:$jsmVersion-$archType" + String faketimeRoot = "/faketimebuild" + String faketimeDockerFilePath = "$faketimeRoot/Dockerfile" + String faketimeAgentFilePath = "$faketimeRoot/faketime.cpp" + String faketimeImageTag = "$imageName-faketime:$jsmVersion-$archType" + String faketimecpp = 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" + faketimecpp + "\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 } @@ -140,7 +185,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..a55fd64 --- /dev/null +++ b/src/main/resources/faketime.cpp @@ -0,0 +1,92 @@ +/* + * 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. + */ + +#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; +} From cc30a7a9be69256f4c6e73e1f0e4997187b73bce Mon Sep 17 00:00:00 2001 From: Joni Lahtinen Date: Wed, 22 Nov 2023 17:56:48 +0200 Subject: [PATCH 2/8] Update docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From febd0016bad837232db11e419caa514af655d1e0 Mon Sep 17 00:00:00 2001 From: Joni Lahtinen Date: Wed, 22 Nov 2023 17:57:14 +0200 Subject: [PATCH 3/8] Do not add x86_64 archType to image tag --- .../groovy/com/eficode/devstack/util/ImageBuilder.groovy | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy index 4bed7b5..a07f3d4 100644 --- a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy +++ b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy @@ -49,7 +49,8 @@ class ImageBuilder extends DoodContainer { 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" @@ -79,11 +80,12 @@ class ImageBuilder extends DoodContainer { 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" String faketimeRoot = "/faketimebuild" String faketimeDockerFilePath = "$faketimeRoot/Dockerfile" String faketimeAgentFilePath = "$faketimeRoot/faketime.cpp" - String faketimeImageTag = "$imageName-faketime:$jsmVersion-$archType" + String faketimeImageTag = "$imageName-faketime:$jsmVersion$archTypeSuffix" String faketimecpp = getClass().getResourceAsStream("/faketime.cpp").text containerName = faketimeImageTag.replaceAll(/[^a-zA-Z0-9_.-]/, "-").take(128-"-IB".length()) containerName += "-IB" From 0296f354a3711eaa6fcd254ec487a97853cc5a18 Mon Sep 17 00:00:00 2001 From: Anders Lantz Date: Thu, 30 Nov 2023 11:41:40 +0100 Subject: [PATCH 4/8] JsmContainerTest.groovy * Fixed broken tests, maninly related to custom aarch64 images --- .../container/impl/JsmContainerTest.groovy | 72 ++++++++++--------- 1 file changed, 38 insertions(+), 34 deletions(-) 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..b2c79fd 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -15,11 +15,11 @@ 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] @@ -30,28 +30,28 @@ class JsmContainerTest extends DevStackSpec { 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") + 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,8 +71,8 @@ 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 == "" } @@ -80,8 +81,13 @@ class JsmContainerTest extends DevStackSpec { 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 + ArrayListexpectedImageTags = ["atlassian/jira-servicemanagement:latest" , "atlassian/jira-servicemanagement:$latestJsmVersion$archTypeSuffix".toString()] when: String containerId = jsm.createContainer() @@ -92,9 +98,9 @@ class JsmContainerTest extends DevStackSpec { 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 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" - DevStackSpec.log.info("\tJSM Container was setup correctly") + log.info("\tJSM Container was setup correctly") where: @@ -108,10 +114,9 @@ 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: @@ -124,8 +129,7 @@ class JsmContainerTest extends DevStackSpec { 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") + log.info("\tJSM Container was setup correctly") where: @@ -141,11 +145,11 @@ class JsmContainerTest extends DevStackSpec { 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" @@ -161,7 +165,7 @@ class JsmContainerTest extends DevStackSpec { 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" @@ -188,42 +192,42 @@ 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) + 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("\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") + log.info("\tCopying files from container appears successful") when: "Copying a file to container" assert jsm.startContainer() : "Error starting container" 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" @@ -234,7 +238,7 @@ class JsmContainerTest extends DevStackSpec { cleanup: - DevStackSpec.log.info("\tDeleting temp dir:" + tempDir.toString()) + log.info("\tDeleting temp dir:" + tempDir.toString()) FileUtils.deleteDirectory(tempDir.toFile()) From 90d19967ca9507979fa78b00328c7bc2228b1bd5 Mon Sep 17 00:00:00 2001 From: Anders Lantz Date: Thu, 30 Nov 2023 12:42:16 +0100 Subject: [PATCH 5/8] JsmContainerTest.groovy * Added basic testing of JVMTimeTravel, mainly making sure the image gets built correctly --- .../container/impl/JsmContainerTest.groovy | 87 ++++++++++++------- 1 file changed, 57 insertions(+), 30 deletions(-) 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 b2c79fd..b86d465 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 @@ -26,7 +27,6 @@ class JsmContainerTest extends DevStackSpec { } - def "test isCreated"(String dockerHost, String certPath) { when: @@ -46,7 +46,7 @@ class JsmContainerTest extends DevStackSpec { log.info("\tisCreated now returns true") when: - jsm.stopAndRemoveContainer() ?: {throw new Exception("Error revoming container $containerId")} + jsm.stopAndRemoveContainer() ?: { throw new Exception("Error revoming container $containerId") } log.info("\tRemoved container") then: @@ -78,7 +78,6 @@ class JsmContainerTest extends DevStackSpec { } - def "test setupContainer"(String dockerHost, String certPath) { setup: log.info("Testing setup of JSM container using trait method") @@ -87,19 +86,19 @@ class JsmContainerTest extends DevStackSpec { String latestJsmVersion = JsmContainer.getLatestJsmVersion() //If arch != x86 a custom image will be built with a tag != latest - ArrayListexpectedImageTags = ["atlassian/jira-servicemanagement:latest" , "atlassian/jira-servicemanagement:$latestJsmVersion$archTypeSuffix".toString()] + 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.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" + 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") @@ -111,7 +110,6 @@ class JsmContainerTest extends DevStackSpec { } - def "test non standard parameters"(String dockerHost, String certPath) { setup: log.info("Testing setup of JSM container using dedicated JSM method") @@ -121,14 +119,14 @@ class JsmContainerTest extends DevStackSpec { 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 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") @@ -140,7 +138,6 @@ class JsmContainerTest extends DevStackSpec { } - def "test stopAndRemoveContainer"(String dockerHost, String certPath) { @@ -160,8 +157,7 @@ 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" @@ -176,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 @@ -203,18 +199,18 @@ class JsmContainerTest extends DevStackSpec { when: "Copying files from container path:" log.info("\tCopying files from container path:" + containerSrcPath) - ArrayListcopiedFiles = jsm.copyFilesFromContainer(containerSrcPath, tempDir.toString() + "/") + 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" + 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() log.info("\tCopying file ($largestFile.name) to container path:" + containerDstDir + largestFile.name) log.debug("\t\tFile size:" + (largestFile.size() * 0.000001).round(1) + "MB") @@ -230,11 +226,10 @@ class JsmContainerTest extends DevStackSpec { 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: @@ -250,6 +245,38 @@ class JsmContainerTest extends DevStackSpec { } + def "Test JVM TimeTravel"(boolean enableTimeTravel, boolean enableJvmDebug) { + + + setup: + log.info("Testing setup and use of JVM TimeTravel enabled JSM container") + JsmContainer jsm = new JsmContainer() + jsm.enableJvmTimeTravel(true) + + when: "Setting up the container JvmTimeTravel enabled" + log.info("\tSetting up JSM container using trait method") + jsm.enableJvmTimeTravel(true) + 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 enabling time travel" + 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 !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 + } } From 3f959a5644ee07dbbf1349f46ee6cebcc5427ceb Mon Sep 17 00:00:00 2001 From: Anders Lantz Date: Thu, 30 Nov 2023 12:51:52 +0100 Subject: [PATCH 6/8] JsmContainer.groovy * WIP, started adding JvmTimeTravel DockerClientDS.groovy * Some more helper methods --- .../container/impl/JsmContainer.groovy | 49 ++++++++++++------- .../devstack/util/DockerClientDS.groovy | 16 ++++++ src/main/resources/faketime.cpp | 2 + .../container/impl/JsmContainerTest.groovy | 5 +- 4 files changed, 53 insertions(+), 19 deletions(-) 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 8cfa564..83fa19d 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 @@ -62,7 +68,7 @@ class JsmContainer implements Container { ContainerCreateRequest setupContainerCreateRequest() { log.debug("Setting up container create request for JSM container") - 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") @@ -71,16 +77,20 @@ class JsmContainer implements Container { 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() + + if (enableJvmTimeTravel) { + log.debug("\tStarting building of Docker Image for faketime JSM") + ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJvmFakeTime(jsmImage, true) + log.debug("\tFinished building custom image:" + faketimeJsmImage.repoTags.join(",")) - log.debug("\tStarting building of Docker Image for faketime JSM verion $jsmVersion") - ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildFaketimeJsm(jsmVersion) - log.debug("\tFinished building custom image:" + faketimeJsmImage.repoTags.join(",")) + imageNameAndTag = faketimeJsmImage.repoTags.first() + } - String image = 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 @@ -88,19 +98,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=-XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_currentTimeMillis -XX:CompileCommand=dontinline,java.lang.System::currentTimeMillis -agentpath:/libfaketime.so=+2592000000 -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 @@ -113,7 +128,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" } } @@ -129,7 +144,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() } @@ -145,9 +160,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) } @@ -169,7 +184,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) } @@ -187,7 +202,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) { @@ -195,8 +210,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/resources/faketime.cpp b/src/main/resources/faketime.cpp index a55fd64..585ba7c 100644 --- a/src/main/resources/faketime.cpp +++ b/src/main/resources/faketime.cpp @@ -12,6 +12,8 @@ * 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 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 b86d465..e21f749 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -245,7 +245,8 @@ class JsmContainerTest extends DevStackSpec { } - def "Test JVM TimeTravel"(boolean enableTimeTravel, boolean enableJvmDebug) { + //Does not test functionality + def "Test building of JVM TimeTravel"(boolean enableTimeTravel, boolean enableJvmDebug) { setup: @@ -264,7 +265,7 @@ class JsmContainerTest extends DevStackSpec { jsm.startContainer() - then: "The container should have envs enabling time travel" + then: "The container should have envs and files enabling time travel" 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" From 75c286a88a03029bd5c2c890c3871e3d84635e24 Mon Sep 17 00:00:00 2001 From: Anders Lantz Date: Thu, 30 Nov 2023 12:55:43 +0100 Subject: [PATCH 7/8] JsmContainerTest.groovy * Impproved timetravel jsm builder test --- .../container/impl/JsmContainerTest.groovy | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 e21f749..98a6a14 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -246,17 +246,17 @@ class JsmContainerTest extends DevStackSpec { //Does not test functionality - def "Test building of JVM TimeTravel"(boolean enableTimeTravel, boolean enableJvmDebug) { + 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() - jsm.enableJvmTimeTravel(true) + when: "Setting up the container JvmTimeTravel enabled" log.info("\tSetting up JSM container using trait method") - jsm.enableJvmTimeTravel(true) + jsm.enableJvmTimeTravel(enableTimeTravel) enableJvmDebug ? jsm.enableJvmDebug() : null String containerId = jsm.createContainer() ContainerSummary containerSummary = dockerClient.getContainerById(containerId) @@ -266,10 +266,10 @@ class JsmContainerTest extends DevStackSpec { jsm.startContainer() then: "The container should have envs and files enabling time travel" - 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 !enableJvmDebug || jvmArgs.contains("-Xdebug") : "JVM Debug was enabled but not is missing from env vars" + 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 " @@ -277,6 +277,7 @@ class JsmContainerTest extends DevStackSpec { enableTimeTravel | enableJvmDebug true | false true | true + false | false } From ff586c96901adf35173867ee4476e356234c5d01 Mon Sep 17 00:00:00 2001 From: Anders Date: Tue, 9 Jan 2024 18:52:26 +0100 Subject: [PATCH 8/8] ImageBuilder.groovy * Created generic method for building fake time image JsmH2DeploymentTest.groovy * Created test for proving JVM time travel works but currently failes --- pom.xml | 2 +- .../container/impl/JsmContainer.groovy | 3 +- .../eficode/devstack/util/ImageBuilder.groovy | 91 ++++++++++++++++--- .../container/impl/JsmContainerTest.groovy | 2 +- .../impl/JsmH2DeploymentTest.groovy | 87 ++++++++++++++++-- 5 files changed, 162 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index fa0adfd..d3b8f22 100644 --- a/pom.xml +++ b/pom.xml @@ -102,7 +102,7 @@ com.eficode.atlassian jirainstancemanager - 2.0.3-SNAPSHOT + 2.0.9-SNAPSHOT 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 83fa19d..5007718 100644 --- a/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy +++ b/src/main/groovy/com/eficode/devstack/container/impl/JsmContainer.groovy @@ -65,6 +65,7 @@ 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") @@ -81,7 +82,7 @@ class JsmContainer implements Container { if (enableJvmTimeTravel) { log.debug("\tStarting building of Docker Image for faketime JSM") - ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJvmFakeTime(jsmImage, true) + ImageSummary faketimeJsmImage = new ImageBuilder(dockerClient.host, dockerClient.certPath).buildJvmFakeTime(jsmImage, false) log.debug("\tFinished building custom image:" + faketimeJsmImage.repoTags.join(",")) imageNameAndTag = faketimeJsmImage.repoTags.first() diff --git a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy index a07f3d4..226cbf9 100644 --- a/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy +++ b/src/main/groovy/com/eficode/devstack/util/ImageBuilder.groovy @@ -76,18 +76,19 @@ class ImageBuilder extends DoodContainer { return newImage } - ImageSummary buildFaketimeJsm(String jsmVersion, boolean force = false){ + /* + 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 faketimecpp = getClass().getResourceAsStream("/faketime.cpp").text - containerName = faketimeImageTag.replaceAll(/[^a-zA-Z0-9_.-]/, "-").take(128-"-IB".length()) + 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") @@ -95,13 +96,13 @@ class ImageBuilder extends DoodContainer { //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]} + ImageSummary existingImage = existingImages.find {it.repoTags == [fakeTimeImageTag]} if (existingImage) { return existingImage } } - String faketimeDockerFile = """ + String fakeTimeDockerFile = """ FROM $imageTag WORKDIR / RUN apt-get update && apt-get install -y wget g++ make @@ -113,19 +114,81 @@ class ImageBuilder extends DoodContainer { """ - putBuilderCommand("mkdir -p $faketimeRoot", "") - putBuilderCommand("cat > $faketimeDockerFilePath <<- 'EOF'\n" + faketimeDockerFile + "\nEOF", "") - putBuilderCommand("cat > $faketimeAgentFilePath <<- 'EOF'\n" + faketimecpp + "\nEOF", "") - putBuilderCommand("cd $faketimeRoot && docker build --tag $faketimeImageTag --build-arg JIRA_VERSION=$jsmVersion --build-arg ARTEFACT_NAME=$artifactName . && echo status:\$?", "status:0") + 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]} + 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 + + } + /** * Will check out Atlassian docker repo and build a Bitbucket image matching docker engine CPU arch. 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 98a6a14..eed52b5 100644 --- a/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy +++ b/src/test/groovy/com/eficode/devstack/container/impl/JsmContainerTest.groovy @@ -245,7 +245,7 @@ class JsmContainerTest extends DevStackSpec { } - //Does not test functionality + //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) { 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 + } + }