diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e17b21c6..e79bf576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Check Docker + run: docker info - name: Checkout uses: actions/checkout@v2 with: @@ -30,6 +32,12 @@ jobs: with: name: test-reports path: build/reports/tests + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-artifacts + path: build/test-artifacts release: runs-on: ubuntu-latest permissions: diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..62593409 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +1.8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fd093d..64c85ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,6 +188,19 @@ Empty release to test changes in release process. ### Deprecated - `Database.setup(ssh: SshConnection): String` in favor of `Database.performSetup(ssh: SshConnection): DatabaseSetup` +### Added +Fix [JPERF-273]: +- Allow multiple ways of installing Jira via `JiraInstallation` or starting it via `JiraStart`. +- Represent the information required to use an already installed Jira via `InstalledJira` or `JiraStart` if started. +- Represent a brand-new Jira instance via `EmptyJiraHome`. +- Hook into Jira installation via `PreInstallHooks` and `PostInstallHooks`. +- Hook into Jira start via `PreStartHooks` and `PostStartHooks`. +- Let hooks insert new hooks. +- Locate and download any logs, charts, profiles and other reports via `Report` (rather than hardcoding the paths). +- Expose preset `NfsSharedHome` or `SambaSharedHome` for Data Center. + +[JPERF-273]: https://ecosystem.atlassian.net/browse/JPERF-273 + ## [4.18.0] - 2021-04-14 [4.18.0]: https://github.com/atlassian/infrastructure/compare/release-4.17.5...release-4.18.0 diff --git a/build.gradle.kts b/build.gradle.kts index 14cd364d..fba3fe7f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,10 +33,16 @@ configurations.all { "org.jetbrains:annotations" -> useVersion("15.0") "com.google.code.findbugs:jsr305" -> useVersion("3.0.2") "org.apache.commons:commons-compress" -> useVersion("1.9") + "commons-io:commons-io" -> useVersion("2.6") + "org.apache.commons:commons-lang3" -> useVersion("3.12.0") } when (requested.group) { "org.jetbrains.kotlin" -> useVersion(kotlinVersion) "org.apache.logging.log4j" -> useVersion(log4jVersion) + "org.bouncycastle" -> useVersion("1.64") + // Gradle 5 treats this module as conflicted, even though the range is compatible + // perhaps Gradle 6 will handle it better + "com.github.docker-java" -> useVersion("3.2.13") } } @@ -70,13 +76,16 @@ dependencies { testCompile("com.atlassian.performance.tools:ssh-ubuntu:0.3.0") testCompile("org.rnorth.duct-tape:duct-tape:1.0.8") testCompile("org.threeten:threeten-extra:1.5.0") + testCompile("com.github.docker-java:docker-java-core:[3.2.5, 4.0.0)") + testCompile("com.github.docker-java:docker-java-transport-zerodep:[3.2.5, 4.0.0)") } fun webdriver(): List = listOf( "selenium-support", "selenium-chrome-driver" ).map { module -> - "org.seleniumhq.selenium:$module:3.141.59" + "org.seleniumhq.selenium:$module:[3.11.0, 3.999.999]" + // it's not `4.0.0)`, because their release channel is polluted with unstable versions like 4.0.0-alpha-1 } fun log4j( diff --git a/gradle/dependency-locks/apiDependenciesMetadata.lockfile b/gradle/dependency-locks/apiDependenciesMetadata.lockfile index a9a97519..d04bdf03 100644 --- a/gradle/dependency-locks/apiDependenciesMetadata.lockfile +++ b/gradle/dependency-locks/apiDependenciesMetadata.lockfile @@ -23,7 +23,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 net.bytebuddy:byte-buddy:1.8.15 @@ -31,7 +31,7 @@ net.i2p.crypto:eddsa:0.2.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -39,8 +39,8 @@ org.apache.logging.log4j:log4j-api:2.17.2 org.apache.logging.log4j:log4j-core:2.17.2 org.apache.logging.log4j:log4j-jul:2.17.2 org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.glassfish:javax.json:1.1 diff --git a/gradle/dependency-locks/compileClasspath.lockfile b/gradle/dependency-locks/compileClasspath.lockfile index feadccbc..defb2400 100644 --- a/gradle/dependency-locks/compileClasspath.lockfile +++ b/gradle/dependency-locks/compileClasspath.lockfile @@ -18,14 +18,14 @@ com.jcraft:jzlib:1.1.3 com.squareup.okhttp3:okhttp:3.11.0 com.squareup.okio:okio:1.14.0 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 javax.inject:javax.inject:1 javax.json:javax.json-api:1.1 net.bytebuddy:byte-buddy:1.8.15 net.i2p.crypto:eddsa:0.2.0 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 org.apache.logging.log4j:log4j-api:2.17.2 @@ -49,8 +49,8 @@ org.apache.maven:maven-repository-metadata:3.5.2 org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/default.lockfile b/gradle/dependency-locks/default.lockfile index b4b93fcf..9b6b1577 100644 --- a/gradle/dependency-locks/default.lockfile +++ b/gradle/dependency-locks/default.lockfile @@ -23,7 +23,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 javax.inject:javax.inject:1 @@ -33,7 +33,7 @@ net.i2p.crypto:eddsa:0.2.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -59,8 +59,8 @@ org.apache.maven:maven-repository-metadata:3.5.2 org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/implementationDependenciesMetadata.lockfile b/gradle/dependency-locks/implementationDependenciesMetadata.lockfile index b4b93fcf..9b6b1577 100644 --- a/gradle/dependency-locks/implementationDependenciesMetadata.lockfile +++ b/gradle/dependency-locks/implementationDependenciesMetadata.lockfile @@ -23,7 +23,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 javax.inject:javax.inject:1 @@ -33,7 +33,7 @@ net.i2p.crypto:eddsa:0.2.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -59,8 +59,8 @@ org.apache.maven:maven-repository-metadata:3.5.2 org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/runtimeClasspath.lockfile b/gradle/dependency-locks/runtimeClasspath.lockfile index b4b93fcf..9b6b1577 100644 --- a/gradle/dependency-locks/runtimeClasspath.lockfile +++ b/gradle/dependency-locks/runtimeClasspath.lockfile @@ -23,7 +23,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 javax.inject:javax.inject:1 @@ -33,7 +33,7 @@ net.i2p.crypto:eddsa:0.2.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -59,8 +59,8 @@ org.apache.maven:maven-repository-metadata:3.5.2 org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/testApiDependenciesMetadata.lockfile b/gradle/dependency-locks/testApiDependenciesMetadata.lockfile index d8a54931..c8dd00f5 100644 --- a/gradle/dependency-locks/testApiDependenciesMetadata.lockfile +++ b/gradle/dependency-locks/testApiDependenciesMetadata.lockfile @@ -10,7 +10,10 @@ com.atlassian.performance.tools:ssh-ubuntu:0.3.0 com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -22,20 +25,22 @@ com.hierynomus:sshj:0.23.0 com.jcraft:jzlib:1.1.3 com.squareup.okhttp3:okhttp:3.11.0 com.squareup.okio:okio:1.14.0 +commons-io:commons-io:2.6 junit:junit:4.13.2 net.bytebuddy:byte-buddy:1.8.15 net.i2p.crypto:eddsa:0.2.0 net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-exec:1.3 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.logging.log4j:log4j-api:2.17.2 org.apache.logging.log4j:log4j-core:2.17.2 org.apache.logging.log4j:log4j-jul:2.17.2 org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.glassfish:javax.json:1.1 diff --git a/gradle/dependency-locks/testCompile.lockfile b/gradle/dependency-locks/testCompile.lockfile index d8a54931..c8dd00f5 100644 --- a/gradle/dependency-locks/testCompile.lockfile +++ b/gradle/dependency-locks/testCompile.lockfile @@ -10,7 +10,10 @@ com.atlassian.performance.tools:ssh-ubuntu:0.3.0 com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -22,20 +25,22 @@ com.hierynomus:sshj:0.23.0 com.jcraft:jzlib:1.1.3 com.squareup.okhttp3:okhttp:3.11.0 com.squareup.okio:okio:1.14.0 +commons-io:commons-io:2.6 junit:junit:4.13.2 net.bytebuddy:byte-buddy:1.8.15 net.i2p.crypto:eddsa:0.2.0 net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-exec:1.3 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.logging.log4j:log4j-api:2.17.2 org.apache.logging.log4j:log4j-core:2.17.2 org.apache.logging.log4j:log4j-jul:2.17.2 org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.glassfish:javax.json:1.1 diff --git a/gradle/dependency-locks/testCompileClasspath.lockfile b/gradle/dependency-locks/testCompileClasspath.lockfile index ba80cded..bd533a68 100644 --- a/gradle/dependency-locks/testCompileClasspath.lockfile +++ b/gradle/dependency-locks/testCompileClasspath.lockfile @@ -11,7 +11,10 @@ com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance.tools:virtual-users:3.14.1 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -24,7 +27,7 @@ com.jcraft:jzlib:1.1.3 com.squareup.okhttp3:okhttp:3.11.0 com.squareup.okio:okio:1.14.0 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 javax.inject:javax.inject:1 javax.json:javax.json-api:1.1 @@ -34,7 +37,7 @@ net.i2p.crypto:eddsa:0.2.0 net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 org.apache.logging.log4j:log4j-api:2.17.2 @@ -59,8 +62,8 @@ org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile b/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile index 2d726e03..414fb3e6 100644 --- a/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile +++ b/gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile @@ -12,7 +12,10 @@ com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance.tools:virtual-users:3.14.1 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -28,7 +31,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 javax.inject:javax.inject:1 @@ -40,7 +43,7 @@ net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -67,8 +70,8 @@ org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/gradle/dependency-locks/testRuntime.lockfile b/gradle/dependency-locks/testRuntime.lockfile index d8a54931..c8dd00f5 100644 --- a/gradle/dependency-locks/testRuntime.lockfile +++ b/gradle/dependency-locks/testRuntime.lockfile @@ -10,7 +10,10 @@ com.atlassian.performance.tools:ssh-ubuntu:0.3.0 com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -22,20 +25,22 @@ com.hierynomus:sshj:0.23.0 com.jcraft:jzlib:1.1.3 com.squareup.okhttp3:okhttp:3.11.0 com.squareup.okio:okio:1.14.0 +commons-io:commons-io:2.6 junit:junit:4.13.2 net.bytebuddy:byte-buddy:1.8.15 net.i2p.crypto:eddsa:0.2.0 net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-exec:1.3 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.logging.log4j:log4j-api:2.17.2 org.apache.logging.log4j:log4j-core:2.17.2 org.apache.logging.log4j:log4j-jul:2.17.2 org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.glassfish:javax.json:1.1 diff --git a/gradle/dependency-locks/testRuntimeClasspath.lockfile b/gradle/dependency-locks/testRuntimeClasspath.lockfile index 2d726e03..414fb3e6 100644 --- a/gradle/dependency-locks/testRuntimeClasspath.lockfile +++ b/gradle/dependency-locks/testRuntimeClasspath.lockfile @@ -12,7 +12,10 @@ com.atlassian.performance.tools:ssh:2.4.3 com.atlassian.performance.tools:virtual-users:3.14.1 com.atlassian.performance:selenium-js:1.0.1 com.fasterxml.jackson.core:jackson-annotations:2.10.3 +com.fasterxml.jackson.core:jackson-core:2.10.3 +com.fasterxml.jackson.core:jackson-databind:2.10.3 com.github.docker-java:docker-java-api:3.2.13 +com.github.docker-java:docker-java-core:3.2.13 com.github.docker-java:docker-java-transport-zerodep:3.2.13 com.github.docker-java:docker-java-transport:3.2.13 com.github.stephenc.jcip:jcip-annotations:1.0-1 @@ -28,7 +31,7 @@ com.squareup.okio:okio:1.14.0 com.typesafe:config:1.2.1 commons-cli:commons-cli:1.4 commons-codec:commons-codec:1.10 -commons-io:commons-io:2.5 +commons-io:commons-io:2.6 commons-logging:commons-logging:1.2 io.github.bonigarcia:webdrivermanager:1.7.1 javax.inject:javax.inject:1 @@ -40,7 +43,7 @@ net.java.dev.jna:jna:5.8.0 org.apache.commons:commons-compress:1.9 org.apache.commons:commons-csv:1.3 org.apache.commons:commons-exec:1.3 -org.apache.commons:commons-lang3:3.5 +org.apache.commons:commons-lang3:3.12.0 org.apache.commons:commons-math3:3.6.1 org.apache.httpcomponents:httpclient:4.5.5 org.apache.httpcomponents:httpcore:4.4.9 @@ -67,8 +70,8 @@ org.apache.maven:maven-resolver-provider:3.5.2 org.apache.maven:maven-settings-builder:3.5.2 org.apache.maven:maven-settings:3.5.2 org.assertj:assertj-core:3.11.1 -org.bouncycastle:bcpkix-jdk15on:1.56 -org.bouncycastle:bcprov-jdk15on:1.56 +org.bouncycastle:bcpkix-jdk15on:1.64 +org.bouncycastle:bcprov-jdk15on:1.64 org.checkerframework:checker-compat-qual:2.0.0 org.codehaus.mojo:animal-sniffer-annotations:1.14 org.codehaus.plexus:plexus-component-annotations:1.7.1 diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/Sed.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/Sed.kt index 082c6a05..f6ac9766 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/Sed.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/Sed.kt @@ -11,6 +11,6 @@ class Sed { ) { val escapedExpression = expression.replace("/", "\\/") val escapedOutput = output.replace("/", "\\/") - connection.execute("sed -i -r 's/$escapedExpression/$escapedOutput/g' $file") + connection.execute("sudo sed -i -r 's/$escapedExpression/$escapedOutput/g' $file") } } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabase.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabase.kt index c24c5a28..8ef841f4 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabase.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabase.kt @@ -23,7 +23,7 @@ class MinimalMysqlDatabase private constructor( override fun start(jira: URI, ssh: SshConnection) { val client = Mysql.installClient(ssh) - Mysql.awaitDatabase(ssh) + Mysql.awaitDatabase(ssh, client) // Based on [jira docs](https://confluence.atlassian.com/adminjiraserver/connecting-jira-applications-to-mysql-5-7-966063305.html) client.runSql(ssh, "CREATE USER 'jiradbuser'@'%' IDENTIFIED BY '$jiraDbUserPassword';") diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt index 34c37b40..40f3ad9a 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabase.kt @@ -43,7 +43,7 @@ class MySqlDatabase( override fun start(jira: URI, ssh: SshConnection) { val client = Mysql.installClient(ssh) - Mysql.awaitDatabase(ssh) + Mysql.awaitDatabase(ssh, client) client.runSql(ssh, "UPDATE jiradb.propertystring SET propertyvalue = '$jira' WHERE id IN (select id from jiradb.propertyentry where property_key like '%baseurl%');") } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/docker/Docker.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/docker/Docker.kt new file mode 100644 index 00000000..70c65ae9 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/docker/Docker.kt @@ -0,0 +1,64 @@ +package com.atlassian.performance.tools.infrastructure.api.docker + +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction +import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.time.Duration + +class Docker private constructor( + private val dependencyPackagesTimeout: Duration, + private val mainPackageTimeout: Duration +) { + + private val ubuntu = Ubuntu() + + /** + * See the [official guide](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/#install-docker-ce). + */ + fun install( + ssh: SshConnection + ) { + ubuntu.install( + ssh = ssh, + packages = listOf( + "apt-transport-https", + "ca-certificates", + "curl" + ), + timeout = dependencyPackagesTimeout + ) + ubuntu.addKey(ssh, "7EA0A9C3F273FCD8") + + val release = ubuntu.getDistributionCodename(ssh) + ubuntu.addRepository( + ssh, + "deb [arch=amd64] https://download.docker.com/linux/ubuntu $release stable", + "docker" + ); + + val version = "5:19.03.13~3-0~ubuntu-$release" + ubuntu.install( + ssh = ssh, + packages = listOf("docker-ce=$version"), + timeout = mainPackageTimeout + ) + ssh.execute("sudo mkdir -p /etc/docker") + ssh.execute("echo '{\"storage-driver\": \"vfs\"}' | sudo tee /etc/docker/daemon.json") + ssh.execute("sudo service docker status || sudo service docker start") + IdempotentAction("poll docker") { + ssh.execute("sudo docker ps") + }.retry(2, StaticBackoff(Duration.ofSeconds(1))) + } + + class Builder { + + private var dependencyPackagesTimeout: Duration = Duration.ofMinutes(2) + private var mainPackageTimeout: Duration = Duration.ofMinutes(5) + + fun build(): Docker = Docker( + dependencyPackagesTimeout, + mainPackageTimeout + ) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/HttpNode.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/HttpNode.kt new file mode 100644 index 00000000..c4d0a9b4 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/HttpNode.kt @@ -0,0 +1,19 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.install + +import java.net.URI + +class HttpNode( + val tcp: TcpNode, + private val basePath: String, + private val supportsTls: Boolean +) { + + fun addressPublicly(): URI = address(tcp.publicIp) + fun addressPrivately(): URI = address(tcp.privateIp) + fun addressPrivately(userName: String, password: String): URI = address(tcp.privateIp, "$userName:$password@") + + private fun address(ip: String, userInfo: String = ""): URI { + val scheme = if (supportsTls) "https" else "http" + return URI("$scheme://$userInfo$ip:${tcp.port}$basePath") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/InstalledJira.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt similarity index 76% rename from src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/InstalledJira.kt rename to src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt index f989223e..92dd76c3 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/InstalledJira.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/InstalledJira.kt @@ -1,10 +1,12 @@ -package com.atlassian.performance.tools.infrastructure.jira.install +package com.atlassian.performance.tools.infrastructure.api.jira.install import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit import com.atlassian.performance.tools.infrastructure.api.os.RemotePath /** * Points to an already installed Jira. + * + * @since 4.19.0 */ class InstalledJira( /** @@ -20,7 +22,7 @@ class InstalledJira( */ val jdk: JavaDevelopmentKit, /** - * Hosts Jira. Specifies sockets used by Jira to handle requests. + * Connects to Jira on HTTP level or below. */ - val server: TcpServer + val http: HttpNode ) diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt new file mode 100644 index 00000000..d34f1a76 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/JiraInstallation.kt @@ -0,0 +1,22 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.install + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import net.jcip.annotations.ThreadSafe + +/** + * @since 4.19.0 + */ +@ThreadSafe +interface JiraInstallation { + + /** + * Installs Jira on [http] node. + * + * @param [http] will host the Jira + * @param [reports] accumulates reports + */ + fun install( + http: HttpNode, + reports: Reports + ): InstalledJira +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/ParallelInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt similarity index 78% rename from src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/ParallelInstallation.kt rename to src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt index 8dcd537e..a6d9b29c 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/ParallelInstallation.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/ParallelInstallation.kt @@ -1,11 +1,13 @@ -package com.atlassian.performance.tools.infrastructure.jira.install +package com.atlassian.performance.tools.infrastructure.api.jira.install import com.atlassian.performance.tools.concurrency.api.submitWithLogContext import com.atlassian.performance.tools.infrastructure.api.distribution.ProductDistribution import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit import com.atlassian.performance.tools.infrastructure.downloadRemotely import com.atlassian.performance.tools.infrastructure.installRemotely +import com.atlassian.performance.tools.infrastructure.jira.install.TomcatConfig import java.util.concurrent.Executors class ParallelInstallation( @@ -15,9 +17,10 @@ class ParallelInstallation( ) : JiraInstallation { override fun install( - server: TcpServer + http: HttpNode, + reports: Reports ): InstalledJira { - server.ssh.newConnection().use { ssh -> + http.tcp.ssh.newConnection().use { ssh -> val pool = Executors.newCachedThreadPool { runnable -> Thread(runnable, "jira-installation-${runnable.hashCode()}") } @@ -30,8 +33,9 @@ class ParallelInstallation( val java = pool.submitWithLogContext("java") { jdk.also { it.install(ssh) } } - val jira = InstalledJira(home.get(), product.get(), java.get(), server) + val jira = InstalledJira(home.get(), product.get(), java.get(), http) pool.shutdownNow() + TomcatConfig(jira, 8080).fixHttp(ssh) return jira } } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/SequentialInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt similarity index 63% rename from src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/SequentialInstallation.kt rename to src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt index 0d9ca677..46cc6e51 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/SequentialInstallation.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/SequentialInstallation.kt @@ -1,10 +1,12 @@ -package com.atlassian.performance.tools.infrastructure.jira.install +package com.atlassian.performance.tools.infrastructure.api.jira.install import com.atlassian.performance.tools.infrastructure.api.distribution.ProductDistribution import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports import com.atlassian.performance.tools.infrastructure.api.jvm.JavaDevelopmentKit import com.atlassian.performance.tools.infrastructure.downloadRemotely import com.atlassian.performance.tools.infrastructure.installRemotely +import com.atlassian.performance.tools.infrastructure.jira.install.TomcatConfig class SequentialInstallation( private val jiraHomeSource: JiraHomeSource, @@ -13,13 +15,16 @@ class SequentialInstallation( ) : JiraInstallation { override fun install( - server: TcpServer + http: HttpNode, + reports: Reports ): InstalledJira { - server.ssh.newConnection().use { ssh -> + http.tcp.ssh.newConnection().use { ssh -> val installation = productDistribution.installRemotely(ssh, ".") val home = jiraHomeSource.downloadRemotely(ssh) jdk.install(ssh) - return InstalledJira(home, installation, jdk, server) + val jira = InstalledJira(home, installation, jdk, http) + TomcatConfig(jira, 8080).fixHttp(ssh) + return jira } } } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpNode.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpNode.kt new file mode 100644 index 00000000..a69e756a --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/TcpNode.kt @@ -0,0 +1,16 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.install + +import com.atlassian.performance.tools.ssh.api.Ssh + +/** + * Has open TCP sockets. + * @param [ssh] connects to the host via [publicIp] + * @param [port] accepts connections at within the [privateIp] network + */ +class TcpNode( + val publicIp: String, + val privateIp: String, + val port: Int, + val name: String, + val ssh: Ssh +) diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt new file mode 100644 index 00000000..b23e5965 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/FileListing.kt @@ -0,0 +1,18 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.report + +import com.atlassian.performance.tools.ssh.api.SshConnection + +class FileListing( + private val pattern: String +) : Report { + + override fun locate( + ssh: SshConnection + ): List = ssh + .safeExecute("ls $pattern") + .takeIf { it.isSuccessful() } + ?.output + ?.lines() + ?.filter { it.isNotBlank() } + ?: emptyList() +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt new file mode 100644 index 00000000..65059517 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Report.kt @@ -0,0 +1,15 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.report + +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Reports back about remote events. E.g. points to interesting logs, dumps, charts. + */ +interface Report { + + /** + * @param [ssh] connects to the server, which holds interesting data + * @return interesting file paths, which could be downloaded + */ + fun locate(ssh: SshConnection): List +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt new file mode 100644 index 00000000..b9737f42 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/Reports.kt @@ -0,0 +1,69 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.report + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.api.os.RemotePath +import com.atlassian.performance.tools.io.api.ensureDirectory +import com.atlassian.performance.tools.io.api.resolveSafely +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class Reports private constructor( // TODO turn into SPI to allow AWS CLI transport (S3) + private val hostReports: Queue +) { + constructor() : this(ConcurrentLinkedQueue()) + + fun add(report: Report, started: StartedJira) { + add(report, started.installed) + } + + fun add(report: Report, installed: InstalledJira) { + add(report, installed.http) + } + + fun add(report: Report, http: HttpNode) { + hostReports.add(HostReport(http.tcp, report)) + } + + fun add(report: Report, tcp: TcpNode) { + hostReports.add(HostReport(tcp, report)) + } + + fun downloadTo( + localDirectory: Path + ): File { + localDirectory.ensureDirectory() + hostReports.groupBy { it.host }.map { (host, reports) -> + host.ssh.newConnection().use { ssh -> + reports + .flatMap { report -> + report.report.locate(ssh).map { path -> RemotePath(host.ssh.host, path) } + } + .forEach { remotePath -> + localDirectory + .resolveSafely(host.name) + .resolve(remotePath.toLocalRelativePath()) + .normalize() + .let { remotePath.download(it) } + } + } + } + return localDirectory.toFile() + } + + private fun RemotePath.toLocalRelativePath(): Path = Paths.get(path.trimStart('/')) + + fun copy(): Reports { + return Reports(ConcurrentLinkedQueue(hostReports)) + } + + private class HostReport( + val host: TcpNode, + val report: Report + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt new file mode 100644 index 00000000..16edfafa --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/report/StaticReport.kt @@ -0,0 +1,16 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.report + +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Points to a remote SSH **file**. For directories use a [FileListing] instead. + * + * @param [remotePath] Points to a file on a remote system. + * Relative to the SSH shell default directory (predominantly the user home directory). + */ +class StaticReport( + private val remotePath: String +) : Report { + + override fun locate(ssh: SshConnection): List = listOf(remotePath) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraLaunchScript.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt similarity index 69% rename from src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraLaunchScript.kt rename to src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt index 14086413..3ce78b6c 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraLaunchScript.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraLaunchScript.kt @@ -1,13 +1,15 @@ -package com.atlassian.performance.tools.infrastructure.jira.start +package com.atlassian.performance.tools.infrastructure.api.jira.start -import com.atlassian.performance.tools.infrastructure.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports import com.atlassian.performance.tools.ssh.api.Ssh import java.time.Duration class JiraLaunchScript : JiraStart { override fun start( - installed: InstalledJira + installed: InstalledJira, + reports: Reports ): StartedJira { val installation = installed.installation Ssh(installation.host).newConnection().use { ssh -> diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt new file mode 100644 index 00000000..891e000c --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/JiraStart.kt @@ -0,0 +1,23 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.start + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import net.jcip.annotations.ThreadSafe + +/** + * @since 4.19.0 + */ +@ThreadSafe +interface JiraStart { + + /** + * Starts the [installed] Jira. + * + * @param [installed] will start the Jira + * @param [reports] accumulates reports + */ + fun start( + installed: InstalledJira, + reports: Reports + ): StartedJira +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt new file mode 100644 index 00000000..da6f4955 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/start/StartedJira.kt @@ -0,0 +1,8 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.start + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira + +class StartedJira( + val installed: InstalledJira, + val pid: Int +) diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK.kt index d853246f..9bd394fe 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK.kt @@ -1,5 +1,6 @@ package com.atlassian.performance.tools.infrastructure.api.jvm +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu import com.atlassian.performance.tools.jvmtasks.api.ExponentialBackoff import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.ssh.api.SshConnection @@ -18,9 +19,11 @@ class AdoptOpenJDK : VersionedJavaDevelopmentKit { override fun getMajorVersion() = 8 override fun install(connection: SshConnection) { + Ubuntu().install(connection, listOf("curl")) download(connection) connection.execute("tar -xzf $jdkArchive") connection.execute("echo '${use()}' >> ~/.profile") + JdkFonts().install(connection) } override fun use(): String { @@ -30,6 +33,7 @@ class AdoptOpenJDK : VersionedJavaDevelopmentKit { override fun command(options: String) = "${jdkBin}java $options" private fun download(connection: SshConnection) { + Ubuntu().install(connection, listOf("curl")) IdempotentAction( description = "Download AdoptOpenJDK", action = { diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK11.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK11.kt index 3975226b..34030eed 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK11.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/AdoptOpenJDK11.kt @@ -1,5 +1,6 @@ package com.atlassian.performance.tools.infrastructure.api.jvm +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu import com.atlassian.performance.tools.jvmtasks.api.ExponentialBackoff import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.ssh.api.SshConnection @@ -28,6 +29,7 @@ class AdoptOpenJDK11 : VersionedJavaDevelopmentKit { download(connection) connection.execute("tar -xzf $jdkArchive") connection.execute("echo '${use()}' >> ~/.profile") + JdkFonts().install(connection) } override fun use(): String = @@ -36,6 +38,7 @@ class AdoptOpenJDK11 : VersionedJavaDevelopmentKit { override fun command(options: String) = "${jdkBin}java $options" private fun download(connection: SshConnection) { + Ubuntu().install(connection, listOf("curl")) IdempotentAction( description = "Download AdoptOpenJDK", action = { diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JdkFonts.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JdkFonts.kt new file mode 100644 index 00000000..81e3786a --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JdkFonts.kt @@ -0,0 +1,14 @@ +package com.atlassian.performance.tools.infrastructure.api.jvm + +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.ssh.api.SshConnection + +class JdkFonts { + + fun install(ssh: SshConnection) { + val packages = listOf( + "fontconfig" // https://jira.atlassian.com/browse/CONFSRVDEV-8954 + ) + Ubuntu().install(ssh, packages) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/OracleJDK.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/OracleJDK.kt index 1730539b..ce499402 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/OracleJDK.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/OracleJDK.kt @@ -1,12 +1,12 @@ package com.atlassian.performance.tools.infrastructure.api.jvm +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu import com.atlassian.performance.tools.ssh.api.SshConnection import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import java.net.URI import java.time.Duration - class OracleJDK : VersionedJavaDevelopmentKit { private val logger: Logger = LogManager.getLogger(this::class.java) private val jdkUpdate = 131 @@ -29,6 +29,7 @@ class OracleJDK : VersionedJavaDevelopmentKit { download(connection) connection.execute("tar -xzf $jdkArchive") connection.execute("echo '${use()}' >> ~/.profile") + JdkFonts().install(connection) } override fun use(): String = "export PATH=$jreBin:$bin:${'$'}PATH; export JAVA_HOME=$path" @@ -36,6 +37,7 @@ class OracleJDK : VersionedJavaDevelopmentKit { override fun command(options: String) = "${jreBin}java $options" private fun download(connection: SshConnection) { + Ubuntu().install(connection, listOf("curl")) val attempts = 0..3 for (attempt in attempts) { try { diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/loadbalancer/LoadBalancer.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/loadbalancer/LoadBalancer.kt index 4fcc35db..93df5246 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/loadbalancer/LoadBalancer.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/loadbalancer/LoadBalancer.kt @@ -3,7 +3,8 @@ package com.atlassian.performance.tools.infrastructure.api.loadbalancer import java.net.URI import java.time.Duration +@Deprecated("Use com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode instead.") interface LoadBalancer { fun waitUntilHealthy(timeout: Duration) val uri: URI -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/Networked.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/Networked.kt new file mode 100644 index 00000000..27a9ee33 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/Networked.kt @@ -0,0 +1,9 @@ +package com.atlassian.performance.tools.infrastructure.api.network + +interface Networked { + + /** + * @return CIDR + */ + fun subnet(): String +} \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/SshServerRoom.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/SshServerRoom.kt new file mode 100644 index 00000000..90ae5632 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/SshServerRoom.kt @@ -0,0 +1,7 @@ +package com.atlassian.performance.tools.infrastructure.api.network + +import com.atlassian.performance.tools.ssh.api.Ssh + +interface SshServerRoom { + fun serveSsh(name: String): Ssh +} \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/TcpServerRoom.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/TcpServerRoom.kt new file mode 100644 index 00000000..a8c786e6 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/network/TcpServerRoom.kt @@ -0,0 +1,12 @@ +package com.atlassian.performance.tools.infrastructure.api.network + +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode + +interface TcpServerRoom { + + /** + * @return reachable by the caller via [TcpNode.publicIp] and by the rest of the network via [TcpNode.privateIp] + */ + fun serveTcp(name: String): TcpNode + fun serveTcp(name: String, tcpPorts: List, udpPorts: List): TcpNode +} \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/RemotePath.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/RemotePath.kt index e61537bb..7c77e952 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/RemotePath.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/api/os/RemotePath.kt @@ -54,6 +54,15 @@ class RemotePath( return localDestination.toFile() } + fun upload( + localSource: File + ): RemotePath { + Ssh(host, connectivityPatience = 4).newConnection().use { ssh -> + ssh.upload(localSource, path) + } + return this + } + override fun toString(): String { return "RemotePath(host=$host, path='$path')" } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MySql.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MySql.kt index c48843e3..e5467310 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MySql.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MySql.kt @@ -1,15 +1,17 @@ package com.atlassian.performance.tools.infrastructure.database import com.atlassian.performance.tools.infrastructure.api.docker.DockerContainer +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.docker.DeadContainerCheck +import com.atlassian.performance.tools.jvmtasks.api.Backoff +import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction +import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff import com.atlassian.performance.tools.ssh.api.SshConnection -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.Logger import java.time.Duration -import java.time.Instant +import java.time.Duration.ofSeconds internal object Mysql { - private val logger: Logger = LogManager.getLogger(this::class.java) private val ubuntu = Ubuntu() /** @@ -29,6 +31,9 @@ internal object Mysql { "--innodb-log-file-size=2G" ) + private val pollPeriod = Duration.ofMillis(500) + private val maxWait = Duration.ofMinutes(15) + fun installClient(ssh: SshConnection): SshSqlClient { ubuntu.install(ssh, listOf("mysql-client")) return SshMysqlClient() @@ -37,12 +42,14 @@ internal object Mysql { fun container( dataDir: String, extraParameters: Array, - extraArguments: Array + extraArguments: Array, + host: TcpNode? = null, + mysqlVersion: String = "5.7.32" ) = DockerContainer.Builder() - .imageName("mysql:5.7.32") + .imageName("mysql:$mysqlVersion") .pullTimeout(Duration.ofMinutes(5)) .parameters( - "-p 3306:3306", + host?.port?.let { "-p $it:$it" } ?: "-p 3306:3306", "-v `realpath $dataDir`:/var/lib/mysql", *extraParameters ) @@ -52,14 +59,19 @@ internal object Mysql { ) .build() - fun awaitDatabase(ssh: SshConnection) { - val mysqlStart = Instant.now() - while (!ssh.safeExecute("mysql -h 127.0.0.1 -u root -e 'select 1;'").isSuccessful()) { - if (Instant.now() > mysqlStart + Duration.ofMinutes(15)) { - throw RuntimeException("MySQL didn't start in time") - } - logger.debug("Waiting for MySQL...") - Thread.sleep(Duration.ofSeconds(10).toMillis()) - } + fun awaitDatabase(ssh: SshConnection, sqlClient: SshSqlClient) { + val backoff = StaticBackoff(pollPeriod) + awaitDatabase(ssh, sqlClient, backoff) + } + + fun awaitDatabase(ssh: SshConnection, sqlClient: SshSqlClient, containerName: String) { + val backoff = DeadContainerCheck(containerName, ssh, StaticBackoff(pollPeriod)) + awaitDatabase(ssh, sqlClient, backoff) + } + + private fun awaitDatabase(ssh: SshConnection, sqlClient: SshSqlClient, backoff: Backoff) { + val maxAttempts = maxWait.toMillis() / pollPeriod.toMillis() + IdempotentAction("wait for MySQL start") { sqlClient.runSql(ssh, "select 1;") } + .retry(maxAttempts.toInt(), backoff) } } \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveConnector.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveConnector.kt new file mode 100644 index 00000000..06cc4388 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveConnector.kt @@ -0,0 +1,30 @@ +package com.atlassian.performance.tools.infrastructure.database + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction +import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.time.Duration.ofSeconds + +/** + * [docs](https://confluence.atlassian.com/adminjiraserver/connecting-jira-applications-to-mysql-5-7-966063305.html#ConnectingJiraapplicationstoMySQL5.7-driver) + */ +class MysqlFiveConnector : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val connector = "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.40.tar.gz" + IdempotentAction("Download MySQL connector") { + ssh.execute("wget -q $connector") + }.retry(3, StaticBackoff(ofSeconds(5))) + ssh.execute("tar -xzf mysql-connector-java-5.1.40.tar.gz") + ssh.execute("cp mysql-connector-java-5.1.40/mysql-connector-java-5.1.40-bin.jar ${jira.installation.path}/lib") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveDotSevenJiraConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveDotSevenJiraConfig.kt new file mode 100644 index 00000000..99eb410d --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/MysqlFiveDotSevenJiraConfig.kt @@ -0,0 +1,45 @@ +package com.atlassian.performance.tools.infrastructure.database + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.report.StaticReport +import com.atlassian.performance.tools.infrastructure.api.os.RemotePath +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.io.File +import java.nio.file.Files + +/** + * [docs](https://confluence.atlassian.com/adminjiraserver/connecting-jira-applications-to-mysql-5-7-966063305.html#ConnectingJiraapplicationstoMySQL5.7-dbconnectionfields) + */ +class MysqlFiveDotSevenJiraConfig( + private val mysql: TcpNode +) : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val remoteConfig: RemotePath = jira.home.resolve("dbconfig.xml") + reports.add(StaticReport(remoteConfig.path), jira) + val config: String = renderConfig() + val localConfig: File = Files.createTempFile("dbconfig", ".xml") + .toFile() + .also { it.writeText(config) } + remoteConfig.upload(localConfig) + } + + private fun renderConfig(): String { + val configTemplate = javaClass.classLoader.getResourceAsStream("mysql-dbconfig.xml").use { + it!!.bufferedReader().readText() + } + return configTemplate + .replace("dbserver", mysql.privateIp) + .replace(":3306", ":${mysql.port}") + + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClient.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClient.kt index 59bba7bb..cb5ad63a 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClient.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClient.kt @@ -3,14 +3,18 @@ package com.atlassian.performance.tools.infrastructure.database import com.atlassian.performance.tools.ssh.api.SshConnection import java.io.File -internal class SshMysqlClient : SshSqlClient { +internal class SshMysqlClient( + private val address: String = "127.0.0.1", + private val port: Int = 3306, + private val user: String = "root" +) : SshSqlClient { override fun runSql( ssh: SshConnection, sql: String ): SshConnection.SshResult { val quotedSql = sql.quote('"') - return ssh.execute("mysql -h 127.0.0.1 -u root -e $quotedSql") + return ssh.execute("mysql -h $address -P $port -u $user -e $quotedSql") } override fun runSql( @@ -19,7 +23,7 @@ internal class SshMysqlClient : SshSqlClient { ): SshConnection.SshResult { val remoteSqlFile = sql.name ssh.upload(sql, remoteSqlFile) - val result = ssh.execute("mysql -h 127.0.0.1 -u root < $remoteSqlFile") + val result = ssh.execute("mysql -h $address -P $port -u $user < $remoteSqlFile") ssh.execute("rm $remoteSqlFile") return result } diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/docker/DeadContainerCheck.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/docker/DeadContainerCheck.kt new file mode 100644 index 00000000..9d222c46 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/docker/DeadContainerCheck.kt @@ -0,0 +1,20 @@ +package com.atlassian.performance.tools.infrastructure.docker + +import com.atlassian.performance.tools.jvmtasks.api.Backoff +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.time.Duration + +internal class DeadContainerCheck( + private val container: String, + private val ssh: SshConnection, + private val base: Backoff +) : Backoff { + override fun backOff(attempt: Int): Duration { + val status = ssh.execute("sudo docker inspect --format '{{.State.Status}}' $container").output.trim() + if (status == "exited") { + val logs = ssh.execute("sudo docker logs $container").errorOutput.trim() + throw Exception("$container exited, logs: $logs") + } + return base.backOff(attempt) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DatabaseIpConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DatabaseIpConfig.kt new file mode 100644 index 00000000..565e67bf --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DatabaseIpConfig.kt @@ -0,0 +1,30 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.database + +import com.atlassian.performance.tools.infrastructure.api.Sed +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.report.StaticReport +import com.atlassian.performance.tools.ssh.api.SshConnection + +class DatabaseIpConfig( + private val databaseIp: String +) : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val dbConfigXml = jira.home.resolve("dbconfig.xml").path + reports.add(StaticReport(dbConfigXml), jira) + Sed().replace( + connection = ssh, + expression = "(.*(@(//)?|//))" + "([^:/]+)" + "(.*)", + output = """\1$databaseIp\5""", + file = dbConfigXml + ) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DockerMysqlServer.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DockerMysqlServer.kt new file mode 100644 index 00000000..9ed15edd --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/database/DockerMysqlServer.kt @@ -0,0 +1,112 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.database + +import com.atlassian.performance.tools.infrastructure.api.dataset.DatasetPackage +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.database.* +import com.atlassian.performance.tools.infrastructure.database.Mysql +import com.atlassian.performance.tools.infrastructure.database.SshMysqlClient +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.JiraInstance +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PostInstanceHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PostInstanceHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PreInstanceHook +import com.atlassian.performance.tools.ssh.api.Ssh +import com.atlassian.performance.tools.ssh.api.SshConnection + +class DockerMysqlServer private constructor( + private val serverRoom: TcpServerRoom, + private val mysqlVersion: String, + private val source: DatasetPackage, + private val maxConnections: Int, + private val extraSqls: List +) : PreInstanceHook { + + override fun call( + nodes: List, + hooks: PostInstanceHooks, + reports: Reports + ) { + val server = serverRoom.serveTcp("mysql") + val client = server.ssh.newConnection().use { setup(it, server) } + nodes.forEach { node -> + node.postInstall.insert(MysqlFiveDotSevenJiraConfig(server)) + node.postInstall.insert(MysqlFiveConnector()) + } + hooks.insert(FixJiraUriViaMysql(client, server.ssh)) + } + + private fun setup(ssh: SshConnection, server: TcpNode): SshSqlClient { + val mysqlDataLocation = source.download(ssh) + val containerName = Mysql.container( + dataDir = mysqlDataLocation, + mysqlVersion = mysqlVersion, + extraParameters = emptyArray(), + extraArguments = arrayOf( + "--skip-grant-tables", // Recovery mode, as some datasets give no permissions to their root DB user + "--max_connections=$maxConnections" + ), + host = server + ).run(ssh) + Ubuntu().install(ssh, listOf("mysql-client")) + val client = SshMysqlClient("127.0.0.1", server.port) + Mysql.awaitDatabase(ssh, client, containerName) + extraSqls.forEach { client.runSql(ssh, it) } + return client + } + + class Builder( + private var serverRoom: TcpServerRoom, + private var source: DatasetPackage + ) { + + private var mysqlVersion: String = "5.7.32" + private var maxConnections: Int = 151 + private var extraSqls: MutableList = mutableListOf() + + fun serverRoom(serverRoom: TcpServerRoom) = apply { this.serverRoom = serverRoom } + fun mysqlVersion(mysqlVersion: String) = apply { this.mysqlVersion = mysqlVersion } + fun source(source: DatasetPackage) = apply { this.source = source } + fun maxConnections(maxConnections: Int) = apply { this.maxConnections = maxConnections } + fun setPassword(user: String, password: String) = apply { + extraSqls.add("UPDATE jiradb.cwd_user SET credential='$password' WHERE user_name='$user';") + } + + fun resetCaptcha(user: String) = apply { + resetAttribute(user, "login.totalFailedCount") + resetAttribute(user, "login.currentFailedCount") + } + + private fun resetAttribute(user: String, attribute: String) { + val sql = "UPDATE jiradb.cwd_user_attributes SET attribute_value = '0'" + + "WHERE user_id = (SELECT id FROM jiradb.cwd_user WHERE user_name = '$user')" + + "AND attribute_name = '$attribute';" + extraSqls.add(sql) + } + + fun build(): DockerMysqlServer = DockerMysqlServer( + serverRoom, + mysqlVersion, + source, + maxConnections, + ArrayList(extraSqls) + ) + } + + private class FixJiraUriViaMysql( + private val client: SshSqlClient, + private val ssh: Ssh + ) : PostInstanceHook { + + override fun call(instance: JiraInstance, hooks: PostInstanceHooks, reports: Reports) { + ssh.newConnection().use { ssh -> + val db = "jiradb" + val update = "UPDATE $db.propertystring SET propertyvalue = '${instance.address.addressPrivately()}'" + val where = "WHERE id IN (select id from $db.propertyentry where property_key like '%baseurl%')" + client.runSql(ssh, "$update $where;") + } + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/AsyncProfilerHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/AsyncProfilerHook.kt new file mode 100644 index 00000000..c17a6a96 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/AsyncProfilerHook.kt @@ -0,0 +1,60 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Report +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHooks +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.net.URI + +class AsyncProfilerHook : PreInstallHook { + + override fun call( + ssh: SshConnection, + http: HttpNode, + hooks: PreInstallHooks, + reports: Reports + ) { + val directory = "async-profiler" + val downloads = URI("https://github.com/jvm-profiling-tools/async-profiler/releases/download/") + val distribution = downloads.resolve("v1.4/async-profiler-1.4-linux-x64.tar.gz") + ssh.execute("wget -q $distribution") + ssh.execute("mkdir $directory") + ssh.execute("tar -xzf async-profiler-1.4-linux-x64.tar.gz -C $directory") + ssh.execute("sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'") + ssh.execute("sudo sh -c 'echo 0 > /proc/sys/kernel/kptr_restrict'") + val profilerPath = "./$directory/profiler.sh" + val profiler = InstalledAsyncProfiler(profilerPath) + hooks.postStart.insert(profiler) + } +} + +private class InstalledAsyncProfiler( + private val profilerPath: String +) : PostStartHook { + + override fun call( + ssh: SshConnection, + jira: StartedJira, + hooks: PostStartHooks, + reports: Reports + ) { + ssh.execute("$profilerPath -b 20000000 start ${jira.pid}") + val profiler = StartedAsyncProfiler(jira.pid, profilerPath) + reports.add(profiler, jira) + } +} + +private class StartedAsyncProfiler( + private val pid: Int, + private val profilerPath: String +) : Report { + + override fun locate(ssh: SshConnection): List { + val flameGraphFile = "flamegraph.svg" + ssh.execute("$profilerPath stop $pid -o svg > $flameGraphFile") + return listOf(flameGraphFile) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/DisabledAutoBackup.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/DisabledAutoBackup.kt new file mode 100644 index 00000000..50b34110 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/DisabledAutoBackup.kt @@ -0,0 +1,19 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.ssh.api.SshConnection + +class DisabledAutoBackup : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + ssh.execute("echo jira.autoexport=false > ${jira.home.path}/jira-config.properties") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraHomeProperty.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraHomeProperty.kt new file mode 100644 index 00000000..cbd517c8 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraHomeProperty.kt @@ -0,0 +1,20 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.ssh.api.SshConnection + +class JiraHomeProperty : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val properties = "${jira.installation.path}/atlassian-jira/WEB-INF/classes/jira-application.properties" + ssh.execute("echo jira.home=`realpath ${jira.home.path}` > $properties") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraLogs.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraLogs.kt new file mode 100644 index 00000000..5758e5bb --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JiraLogs.kt @@ -0,0 +1,33 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Report +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.nio.file.Path +import java.nio.file.Paths + +class JiraLogs : PostInstallHook { + + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { + reports.add(report(jira), jira) + } + + fun report(jira: InstalledJira): Report { + return JiraLogsReport(jira) + } + + private class JiraLogsReport(private val jira: InstalledJira) : Report { + override fun locate(ssh: SshConnection): List { + return listOf( + "${jira.home.path}/log/atlassian-jira.log", + "${jira.installation.path}/logs/catalina.out" + ).onEach { ensureFile(Paths.get(it), ssh) } + } + + private fun ensureFile(path: Path, ssh: SshConnection) { + ssh.execute("mkdir -p ${path.parent!!}") + ssh.execute("touch $path") + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JvmConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JvmConfig.kt new file mode 100644 index 00000000..dbaaa717 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/JvmConfig.kt @@ -0,0 +1,33 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.JiraGcLog +import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig +import com.atlassian.performance.tools.infrastructure.api.jira.SetenvSh +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.ssh.api.SshConnection + +class JvmConfig( + private val config: JiraNodeConfig +) : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val gcLog = JiraGcLog(jira.installation.path) + SetenvSh(jira.installation.path).setup( + connection = ssh, + config = config, + gcLog = gcLog, + jiraIp = jira.http.tcp.publicIp + ) + val report = FileListing(gcLog.path("*")) + reports.add(report, jira) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/LateUbuntuSysstat.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/LateUbuntuSysstat.kt new file mode 100644 index 00000000..0a3c89c6 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/LateUbuntuSysstat.kt @@ -0,0 +1,37 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.Iostat +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.api.os.OsMetric +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.api.os.Vmstat +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHooks +import com.atlassian.performance.tools.infrastructure.jira.report.RemoteMonitoringProcessReport +import com.atlassian.performance.tools.ssh.api.SshConnection + +class LateUbuntuSysstat : PostInstallHook { + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + val ubuntu = Ubuntu() + ubuntu.install(ssh, listOf("sysstat")) + listOf(Vmstat(), Iostat()) + .map { PostStartOsMetric(it) } + .forEach { hooks.postStart.insert(it) } + } +} + +private class PostStartOsMetric( + private val metric: OsMetric +) : PostStartHook { + override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks, reports: Reports) { + val process = metric.start(ssh) + reports.add(RemoteMonitoringProcessReport(process), jira) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHook.kt new file mode 100644 index 00000000..9eb4fa2b --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHook.kt @@ -0,0 +1,24 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Intercepts a call after Jira is installed. + */ +interface PostInstallHook { + + /** + * @param [ssh] connects to the [jira] + * @param [jira] points to the installed Jira + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHooks.kt new file mode 100644 index 00000000..41277b3e --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PostInstallHooks.kt @@ -0,0 +1,55 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHooks +import com.atlassian.performance.tools.infrastructure.jira.install.hook.ProfilerHook +import com.atlassian.performance.tools.infrastructure.jira.install.hook.SplunkForwarderHook +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PostInstallHooks private constructor( + val preStart: PreStartHooks +) { + + private val hooks: Queue = ConcurrentLinkedQueue() + val postStart = preStart.postStart + + fun insert( + hook: PostInstallHook + ) { + hooks.add(hook) + } + + internal fun call( + ssh: SshConnection, + jira: InstalledJira, + reports: Reports + ) { + while (true) { + hooks + .poll() + ?.call(ssh, jira, this, reports) + ?: break + } + } + + companion object Factory { + fun default(): PostInstallHooks = PostInstallHooks(PreStartHooks.default()).apply { + val config = JiraNodeConfig.Builder().build() + listOf( + JiraHomeProperty(), + DisabledAutoBackup(), + JvmConfig(config), + ProfilerHook(config.profiler), + SplunkForwarderHook(config.splunkForwarder), + JiraLogs(), + LateUbuntuSysstat() + ).forEach { insert(it) } + } + + fun empty(): PostInstallHooks = PostInstallHooks(PreStartHooks.empty()) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHook.kt new file mode 100644 index 00000000..49be4fc9 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHook.kt @@ -0,0 +1,24 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Intercepts a call before Jira is installed. + */ +interface PreInstallHook { + + /** + * @param [ssh] connects to the [http] host + * @param [http] will install Jira + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + ssh: SshConnection, + http: HttpNode, + hooks: PreInstallHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHooks.kt new file mode 100644 index 00000000..db6ead0c --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/PreInstallHooks.kt @@ -0,0 +1,43 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PreInstallHooks private constructor( + val postInstall: PostInstallHooks +) { + + private val hooks: Queue = ConcurrentLinkedQueue() + val preStart = postInstall.preStart + val postStart = preStart.postStart + + fun insert( + hook: PreInstallHook + ) { + hooks.add(hook) + } + + internal fun call( + ssh: SshConnection, + http: HttpNode, + reports: Reports + ) { + while (true) { + hooks + .poll() + ?.call(ssh, http, this, reports) + ?: break + } + } + + companion object Factory { + fun default(): PreInstallHooks = PreInstallHooks(PostInstallHooks.default()).apply { + insert(SystemLog()) + } + + fun empty(): PreInstallHooks = PreInstallHooks(PostInstallHooks.empty()) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/SystemLog.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/SystemLog.kt new file mode 100644 index 00000000..42841fc9 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/install/hook/SystemLog.kt @@ -0,0 +1,15 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.ssh.api.SshConnection + +class SystemLog : PreInstallHook { + + override fun call(ssh: SshConnection, http: HttpNode, hooks: PreInstallHooks, reports: Reports) { + reports.add(FileListing("/var/log/syslog"), http) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/DefaultClusterProperties.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/DefaultClusterProperties.kt new file mode 100644 index 00000000..2561eb37 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/DefaultClusterProperties.kt @@ -0,0 +1,18 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.jira.instance.ClusterProperties +import com.atlassian.performance.tools.ssh.api.SshConnection + +class DefaultClusterProperties : PostInstallHook { + + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { + ClusterProperties(jira).apply { + set("jira.node.id", jira.http.tcp.name, ssh) + set("ehcache.object.port", "40011", ssh) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraDataCenterPlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraDataCenterPlan.kt new file mode 100644 index 00000000..6b5b652a --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraDataCenterPlan.kt @@ -0,0 +1,95 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.loadbalancer.ApacheProxyPlan +import com.atlassian.performance.tools.infrastructure.hookapi.loadbalancer.LoadBalancerPlan +import com.atlassian.performance.tools.infrastructure.hookapi.network.HttpServerRoom +import java.time.Duration +import kotlin.streams.asStream +import kotlin.streams.toList + +class JiraDataCenterPlan private constructor( + private val nodePlans: List, + private val instanceHooks: PreInstanceHooks, + private val balancerPlan: LoadBalancerPlan +) : JiraInstancePlan { + + private val reports: Reports = Reports() + private val loadBalancingPatience = Duration.ofMinutes(5) + + override fun materialize(): JiraInstance { + instanceHooks.call(nodePlans.map { it.hooks }, reports) + val http = nodePlans.mapIndexed { nodeIndex, plan -> + val nodeNumber = nodeIndex + 1 + val http = plan.serverRoom.serveHttp("jira-node-$nodeNumber") + PlannedHttpNode(http, plan) + } + val balancer = balancerPlan.materialize(http.map { it.http }, nodePlans.map { it.hooks.preStart }) + val installed = installInParallel(http) + val started = installed.map { it.start(reports) } + val instance = JiraDataCenter(started, balancer) + instanceHooks.postInstance.call(instance, reports) + return instance + } + + override fun report(): Reports = reports.copy() + + private fun installInParallel(nodes: Collection): List = nodes + .asSequence() + .asStream() + .parallel() + .map { it.install(reports) } + .toList() + + private class PlannedHttpNode( + val http: HttpNode, + val plan: JiraNodePlan + ) { + fun install(reports: Reports): PlannedInstalledJira { + return plan.installation.install(http, reports).let { PlannedInstalledJira(it, plan) } + } + } + + private class PlannedInstalledJira( + val installed: InstalledJira, + val plan: JiraNodePlan + ) { + fun start(reports: Reports): StartedJira { + return plan.start.start(installed, reports) + } + } + + private class JiraDataCenter( + override val nodes: List, + loadBalancer: HttpNode + ) : JiraInstance { + override val address = loadBalancer + } + + class Builder( + private var nodePlans: List, + private var balancerPlan: LoadBalancerPlan + ) { + private var instanceHooks: PreInstanceHooks = PreInstanceHooks.default() + + constructor( + serverRoom: HttpServerRoom + ) : this( + nodePlans = listOf(1, 2).map { + JiraNodePlan.Builder(serverRoom) + .dataCenter() + .build() + }, + balancerPlan = ApacheProxyPlan(serverRoom) + ) + + fun nodePlans(nodePlans: List) = apply { this.nodePlans = nodePlans } + fun instanceHooks(instanceHooks: PreInstanceHooks) = apply { this.instanceHooks = instanceHooks } + fun balancerPlan(balancerPlan: LoadBalancerPlan) = apply { this.balancerPlan = balancerPlan } + + fun build(): JiraInstancePlan = JiraDataCenterPlan(nodePlans, instanceHooks, balancerPlan) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstance.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstance.kt new file mode 100644 index 00000000..73308d21 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstance.kt @@ -0,0 +1,9 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira + +interface JiraInstance { + val address: HttpNode + val nodes: List +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstancePlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstancePlan.kt new file mode 100644 index 00000000..02d8c3f5 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraInstancePlan.kt @@ -0,0 +1,8 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports + +interface JiraInstancePlan { + fun materialize(): JiraInstance + fun report(): Reports +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraNodePlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraNodePlan.kt new file mode 100644 index 00000000..828ce080 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraNodePlan.kt @@ -0,0 +1,65 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.distribution.PublicJiraSoftwareDistribution +import com.atlassian.performance.tools.infrastructure.api.jira.EmptyJiraHome +import com.atlassian.performance.tools.infrastructure.api.jira.install.JiraInstallation +import com.atlassian.performance.tools.infrastructure.api.jira.install.ParallelInstallation +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraLaunchScript +import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraStart +import com.atlassian.performance.tools.infrastructure.api.jvm.OracleJDK +import com.atlassian.performance.tools.infrastructure.hookapi.network.HttpServerRoom +import com.atlassian.performance.tools.infrastructure.jira.install.hook.HookedJiraInstallation +import com.atlassian.performance.tools.infrastructure.jira.start.hook.HookedJiraStart +import net.jcip.annotations.NotThreadSafe + +/** + * Specifies how a Jira node should be built. + * Does not build the node, due to various possible ordering, concurrency and optimizations. + * Actual provisioning is reserved for a [JiraInstancePlan]. + * + * @constructor specifies the plan, but doesn't hold any resources + * @see JiraInstancePlan + * @since 4.19.0 + */ +class JiraNodePlan private constructor( + internal val serverRoom: HttpServerRoom, + internal val installation: JiraInstallation, + internal val start: JiraStart, + internal val hooks: PreInstallHooks +) { + + @NotThreadSafe + class Builder( + private var serverRoom: HttpServerRoom + ) { + private var installation: JiraInstallation = ParallelInstallation( + EmptyJiraHome(), + PublicJiraSoftwareDistribution("7.13.0"), + OracleJDK() + ) + private var start: JiraStart = JiraLaunchScript() + private var hooks: PreInstallHooks = PreInstallHooks.default() + + constructor(plan: JiraNodePlan) : this( + plan.serverRoom + ) { + this.installation = plan.installation + this.start = plan.start + this.hooks = plan.hooks + } + + fun serverRoom(serverRoom: HttpServerRoom) = apply { this.serverRoom = serverRoom } + fun installation(installation: JiraInstallation) = apply { this.installation = installation } + fun start(start: JiraStart) = apply { this.start = start } + fun hooks(hooks: PreInstallHooks) = apply { this.hooks = hooks } + fun dataCenter() = hooks(PreInstallHooks.default().apply { postInstall.insert(DefaultClusterProperties()) }) + + fun build() = JiraNodePlan( + serverRoom, + HookedJiraInstallation(installation, hooks), + HookedJiraStart(start, hooks.preStart), + hooks + ) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraServerPlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraServerPlan.kt new file mode 100644 index 00000000..df3ad0db --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/JiraServerPlan.kt @@ -0,0 +1,50 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.network.HttpServerRoom + +class JiraServerPlan private constructor( + private val plan: JiraNodePlan, + private val hooks: PreInstanceHooks +) : JiraInstancePlan { + + private val reports: Reports = Reports() + + override fun materialize(): JiraInstance { + val nodeHooks = listOf(plan).map { it.hooks } + hooks.call(nodeHooks, reports) + val http = plan.serverRoom.serveHttp("jira-node") + val installed = plan.installation.install(http, reports) + val started = plan.start.start(installed, reports) + val instance = JiraServer(started) + hooks.postInstance.call(instance, reports) + return instance + } + + override fun report(): Reports = reports.copy() + + private class JiraServer( + node: StartedJira + ) : JiraInstance { + override val address: HttpNode = node.installed.http + override val nodes: List = listOf(node) + } + + class Builder( + private var plan: JiraNodePlan + + ) { + private var hooks: PreInstanceHooks = PreInstanceHooks.default() + + constructor(serverRoom: HttpServerRoom) : this( + plan = JiraNodePlan.Builder(serverRoom).build() + ) + + fun plan(plan: JiraNodePlan) = apply { this.plan = plan } + fun hooks(hooks: PreInstanceHooks) = apply { this.hooks = hooks } + + fun build(): JiraInstancePlan = JiraServerPlan(plan, hooks) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHook.kt new file mode 100644 index 00000000..f680bf49 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHook.kt @@ -0,0 +1,17 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports + +interface PostInstanceHook { + + /** + * @param [instance] a standalone Jira Server node or a Jira Data Center cluster + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + instance: JiraInstance, + hooks: PostInstanceHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHooks.kt new file mode 100644 index 00000000..f9480026 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PostInstanceHooks.kt @@ -0,0 +1,29 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PostInstanceHooks private constructor() { + + private val hooks: Queue = ConcurrentLinkedQueue() + + fun insert(hook: PostInstanceHook) { + hooks.add(hook) + } + + fun call(instance: JiraInstance, reports: Reports) { + while (true) { + hooks + .poll() + ?.call(instance, this, reports) + ?: break + } + } + + + companion object Factory { + fun default(): PostInstanceHooks = empty() + fun empty(): PostInstanceHooks = PostInstanceHooks() + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHook.kt new file mode 100644 index 00000000..73feac8e --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHook.kt @@ -0,0 +1,18 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports + +interface PreInstanceHook { + + /** + * @param [nodes] inserts node hooks + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + nodes: List, + hooks: PostInstanceHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHooks.kt new file mode 100644 index 00000000..f9ca9905 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/instance/PreInstanceHooks.kt @@ -0,0 +1,28 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.instance + +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PreInstanceHooks private constructor( + val postInstance: PostInstanceHooks +) { + + private val hooks: Queue = ConcurrentLinkedQueue() + + fun insert(hook: PreInstanceHook) { + hooks.add(hook) + } + + internal fun call(nodes: List, reports: Reports) { + hooks + .parallelStream() + .forEach { it.call(nodes, postInstance, reports) } + } + + companion object Factory { + fun default(): PreInstanceHooks = PreInstanceHooks(PostInstanceHooks.default()) + fun empty(): PreInstanceHooks = PreInstanceHooks(PostInstanceHooks.empty()) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/NfsSharedHome.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/NfsSharedHome.kt new file mode 100644 index 00000000..2638f265 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/NfsSharedHome.kt @@ -0,0 +1,64 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.sharedhome + +import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.network.Networked +import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PostInstanceHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PreInstanceHook +import com.atlassian.performance.tools.infrastructure.jira.sharedhome.SharedHomeProperty +import com.atlassian.performance.tools.ssh.api.SshConnection + +class NfsSharedHome( + private val jiraHomeSource: JiraHomeSource, + private val infrastructure: TcpServerRoom, + private val networked: Networked +) : PreInstanceHook { + private val localHome = "/home/ubuntu/jira-shared-home" + + override fun call(nodes: List, hooks: PostInstanceHooks, reports: Reports) { + val tcp = infrastructure.serveTcp("shared-home") + tcp.ssh.newConnection().use { ssh -> + download(ssh) + export(ssh) + } + nodes.forEach { it.postInstall.insert(NfsMount(tcp, localHome)) } + } + + private fun download(ssh: SshConnection) { + ssh.execute("sudo mkdir -p $localHome") + val jiraHome = jiraHomeSource.download(ssh) + ssh.execute("sudo mv $jiraHome/{data,plugins,import,export} $localHome") + ssh.safeExecute("sudo mv $jiraHome/logos $localHome") + } + + private fun export(ssh: SshConnection): SshConnection.SshResult { + Ubuntu().install(ssh, listOf("nfs-kernel-server")) + val options = "rw,sync,no_subtree_check,no_root_squash" + ssh.execute("sudo echo '$localHome ${networked.subnet()}($options)' | sudo tee -a /etc/exports") + return ssh.execute("sudo service nfs-kernel-server restart") + } + + private class NfsMount( + private val tcp: TcpNode, + private val sharedHome: String + ) : PostInstallHook { + + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { + Ubuntu().install(ssh, listOf("nfs-common")) + val mountSource = "${tcp.privateIp}:$sharedHome" + val mountTarget = "mounted-shared-home" + ssh.execute("mkdir -p $mountTarget") + ssh.execute("sudo mount -o soft,intr,rsize=8192,wsize=8192 $mountSource $mountTarget") + ssh.execute("sudo chown ubuntu:ubuntu $mountTarget") + val mounted = "`realpath $mountTarget`" + SharedHomeProperty(jira).set(mounted, ssh) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/SambaSharedHome.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/SambaSharedHome.kt new file mode 100644 index 00000000..d92f1590 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/sharedhome/SambaSharedHome.kt @@ -0,0 +1,86 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.sharedhome + +import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomeSource +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PostInstanceHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PreInstanceHook +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.jira.sharedhome.SharedHomeProperty +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.util.* + +class SambaSharedHome( + private val jiraHomeSource: JiraHomeSource, + private val serverRoom: TcpServerRoom +) : PreInstanceHook { + + override fun call(nodes: List, hooks: PostInstanceHooks, reports: Reports) { + val server = serverRoom.serveTcp("samba-shared-home", listOf(139, 445), listOf(137, 138)) + val mount = server.ssh.newConnection().use { ssh -> + val sharedHome = download(ssh) + export(ssh, sharedHome, server) + } + nodes.forEach { it.postInstall.insert(mount) } + } + + private fun download(ssh: SshConnection): String { + val sharedHome = "/home/ubuntu/jira-shared-home" + ssh.execute("sudo mkdir -p $sharedHome") + val jiraHome = jiraHomeSource.download(ssh) + ssh.execute("sudo mv $jiraHome/{data,plugins,import,export} $sharedHome") + ssh.safeExecute("sudo mv $jiraHome/logos $sharedHome") + return sharedHome + } + + private fun export(ssh: SshConnection, sharedHome: String, server: TcpNode): SambaMount { + Ubuntu().install(ssh, listOf("samba")) + val shareName = "samba-jira-home" + val share = """ + [$shareName] + comment = shared Jira home + path = $sharedHome + read only = no + browsable = no + """.trimIndent() + ssh.execute("echo '$share' | sudo tee -a /etc/samba/smb.conf") + val user = ssh.execute("whoami").output.trim() + val password = generatePassword() + // could transfer password via file, but it's an ephemeral secret anyway + ssh.execute("echo -e '$password\\n$password\\n' | sudo smbpasswd -s -a $user") + ssh.execute("sudo service smbd restart") + return SambaMount(server.privateIp, shareName, user, password) + } + + private fun generatePassword(): String { + val rng = Random() + val chars = ('a'..'Z') + ('A'..'Z') + ('0'..'9') + return (1..32).map { chars[rng.nextInt(chars.size)] }.joinToString("") + } + + private class SambaMount( + private val ip: String, + private val shareName: String, + private val user: String, + private val password: String + ) : PostInstallHook { + + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { + Ubuntu().install(ssh, listOf("cifs-utils")) + val credentials = "username=$user,password=$password" + val mountSource = "//$ip/$shareName" + val mountTarget = "mounted-shared-home" + ssh.execute("mkdir -p $mountTarget") + val localUser = ssh.execute("whoami").output.trim() + ssh.execute("sudo chown $localUser:$localUser $mountTarget") + ssh.execute("sudo mount -t cifs -o $credentials $mountSource $mountTarget") + val mounted = "`realpath $mountTarget`" + SharedHomeProperty(jira).set(mounted, ssh) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/AccessLogs.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/AccessLogs.kt new file mode 100644 index 00000000..1063e810 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/AccessLogs.kt @@ -0,0 +1,13 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection + +class AccessLogs : PreStartHook { + + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PreStartHooks, reports: Reports) { + reports.add(FileListing("${jira.installation.path}/logs/*access*"), jira) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/JstatHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/JstatHook.kt new file mode 100644 index 00000000..3f7d6adb --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/JstatHook.kt @@ -0,0 +1,19 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.jira.report.RemoteMonitoringProcessReport +import com.atlassian.performance.tools.ssh.api.SshConnection + +class JstatHook : PostStartHook { + + override fun call( + ssh: SshConnection, + jira: StartedJira, + hooks: PostStartHooks, + reports: Reports + ) { + val process = jira.installed.jdk.jstatMonitoring.start(ssh, jira.pid) + reports.add(RemoteMonitoringProcessReport(process), jira) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHook.kt new file mode 100644 index 00000000..a8392e41 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHook.kt @@ -0,0 +1,24 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Intercepts a call after Jira is started. + */ +interface PostStartHook { + + /** + * @param [ssh] connects to the [jira] + * @param [jira] points to the started Jira + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + ssh: SshConnection, + jira: StartedJira, + hooks: PostStartHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHooks.kt new file mode 100644 index 00000000..bb2b6455 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PostStartHooks.kt @@ -0,0 +1,39 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PostStartHooks private constructor() { + + private val hooks: Queue = ConcurrentLinkedQueue() + + fun insert( + hook: PostStartHook + ) { + hooks.add(hook) + } + + internal fun call( + ssh: SshConnection, + jira: StartedJira, + reports: Reports + ) { + while (true) { + hooks + .poll() + ?.call(ssh, jira, this, reports) + ?: break + } + } + + companion object Factory { + fun default(): PostStartHooks = empty().apply { + insert(JstatHook()) + } + + fun empty(): PostStartHooks = PostStartHooks() + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHook.kt new file mode 100644 index 00000000..5313aeb6 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHook.kt @@ -0,0 +1,24 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Intercepts a call before Jira is started. + */ +interface PreStartHook { + + /** + * @param [ssh] connects to the [jira] + * @param [jira] points to the installed Jira + * @param [hooks] inserts future hooks + * @param [reports] accumulates reports + */ + fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PreStartHooks, + reports: Reports + ) +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHooks.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHooks.kt new file mode 100644 index 00000000..b5acd2f1 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/PreStartHooks.kt @@ -0,0 +1,41 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue + +class PreStartHooks private constructor( + val postStart: PostStartHooks +) { + + private val hooks: Queue = ConcurrentLinkedQueue() + + fun insert( + hook: PreStartHook + ) { + hooks.add(hook) + } + + internal fun call( + ssh: SshConnection, + jira: InstalledJira, + reports: Reports + ) { + while (true) { + hooks + .poll() + ?.call(ssh, jira, this, reports) + ?: break + } + } + + companion object Factory { + fun default() = PreStartHooks(PostStartHooks.default()).apply { + insert(AccessLogs()) + } + + fun empty() = PreStartHooks(PostStartHooks.empty()) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/RestUpgrade.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/RestUpgrade.kt new file mode 100644 index 00000000..c949f786 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/jira/start/hook/RestUpgrade.kt @@ -0,0 +1,89 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.JiraLaunchTimeouts +import com.atlassian.performance.tools.infrastructure.api.jira.report.FileListing +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.api.jvm.ThreadDump +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.JiraLogs +import com.atlassian.performance.tools.infrastructure.jira.report.JiraLandingPage +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.net.URI +import java.time.Duration +import java.time.Instant + +class RestUpgrade( + private val timeouts: JiraLaunchTimeouts, + private val adminUsername: String, + private val adminPassword: String +) : PostStartHook { + + override fun call(ssh: SshConnection, jira: StartedJira, hooks: PostStartHooks, reports: Reports) { + val threadDump = ThreadDump(jira.pid, jira.installed.jdk) + val polling = Upgrades(ssh, jira, adminUsername, adminPassword, timeouts, threadDump, reports) + polling.waitUntilOnline() + polling.waitUntilHealthy() + polling.triggerUpgrades() + polling.waitUntilUpgraded() + } + + private class Upgrades( + private val ssh: SshConnection, + private val jira: StartedJira, + adminUsername: String, + adminPassword: String, + private val timeouts: JiraLaunchTimeouts, + private val threadDump: ThreadDump, + private val reports: Reports + ) { + private val upgradesEndpoint: URI = jira + .installed + .http + .addressPrivately(adminUsername, adminPassword) + .resolve("rest/api/2/upgrade") + + fun waitUntilOnline() { + waitForStatusToChange("000", timeouts.offlineTimeout) + } + + fun waitUntilHealthy() { + waitForStatusToChange("503", timeouts.initTimeout) + } + + fun waitUntilUpgraded() { + waitForStatusToChange("303", timeouts.upgradeTimeout) + } + + private fun waitForStatusToChange( + statusQuo: String, + timeout: Duration + ) { + val backoff = Duration.ofSeconds(10) + val deadline = Instant.now() + timeout + while (true) { + val currentStatus = ssh.safeExecute( + cmd = "curl --silent --write-out '%{http_code}' --output /dev/null -X GET $upgradesEndpoint", + timeout = timeouts.unresponsivenessTimeout + ).output + if (currentStatus != statusQuo) { + break + } + if (deadline < Instant.now()) { + reports.add(JiraLogs().report(jira.installed), jira) + reports.add(FileListing("thread-dumps/*"), jira) + reports.add(JiraLandingPage(jira), jira) + throw Exception("$upgradesEndpoint failed to get out of $statusQuo status within $timeout") + } + threadDump.gather(ssh, "thread-dumps") + Thread.sleep(backoff.toMillis()) + } + } + + fun triggerUpgrades() { + ssh.execute( + cmd = "curl --silent --retry 6 -X POST $upgradesEndpoint", + timeout = Duration.ofSeconds(15) + ) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/ApacheProxyPlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/ApacheProxyPlan.kt new file mode 100644 index 00000000..4f83c125 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/ApacheProxyPlan.kt @@ -0,0 +1,75 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.loadbalancer + +import com.atlassian.performance.tools.infrastructure.api.Sed +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHooks +import com.atlassian.performance.tools.infrastructure.hookapi.network.HttpServerRoom +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.jvmtasks.api.ExponentialBackoff +import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction +import com.atlassian.performance.tools.ssh.api.SshConnection +import java.net.URI +import java.time.Duration + +class ApacheProxyPlan( + private val serverRoom: HttpServerRoom +) : LoadBalancerPlan { + + private val configPath = "/etc/apache2/sites-enabled/000-default.conf" + + override fun materialize(nodes: List, hooks: List): HttpNode { + val proxyNode = serverRoom.serveHttp("apache-proxy") + IdempotentAction("Installing and configuring apache load balancer") { + proxyNode.tcp.ssh.newConnection().use { connection -> + tryToProvision(connection, nodes, proxyNode) + } + }.retry(2, ExponentialBackoff(Duration.ofSeconds(5))) + val balancerEndpoint = proxyNode.addressPrivately() + hooks.forEach { it.insert(InjectProxy(balancerEndpoint)) } + return proxyNode + } + + private fun tryToProvision(ssh: SshConnection, nodes: List, proxyNode: HttpNode) { + Ubuntu().install(ssh, listOf("apache2")) + Sed().replace(ssh, "Listen 80", "Listen ${proxyNode.tcp.port}", "/etc/apache2/ports.conf") + ssh.execute("sudo rm $configPath") + ssh.execute("sudo touch $configPath") + val mods = listOf( + "proxy", "proxy_ajp", "proxy_http", "rewrite", "deflate", "headers", "proxy_balancer", "proxy_connect", + "proxy_html", "xml2enc", "lbmethod_byrequests" + ) + ssh.execute("sudo a2enmod ${mods.joinToString(" ")}") + appendConfig( + ssh, + "Header add Set-Cookie \\\"ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/\\\" env=BALANCER_ROUTE_CHANGED" + ) + appendConfig(ssh, "") + nodes.forEachIndexed { index, http -> + appendConfig(ssh, "\tBalancerMember ${http.addressPrivately()} route=$index") + } + appendConfig(ssh, "\n") + appendConfig(ssh, "ProxyPass / balancer://mycluster/ stickysession=ROUTEID") + appendConfig(ssh, "ProxyPassReverse / balancer://mycluster/ stickysession=ROUTEID") + ssh.execute("sudo service apache2 restart", Duration.ofMinutes(3)) + } + + private fun appendConfig(connection: SshConnection, line: String) { + connection.execute("echo \"$line\" | sudo tee -a $configPath") + } + + private class InjectProxy( + private val proxy: URI + ) : PreStartHook { + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PreStartHooks, reports: Reports) { + Sed().replace( + ssh, + "bindOnInit=\"false\"", + "bindOnInit=\"false\" scheme=\"http\" proxyName=\"${proxy.host}\" proxyPort=\"${proxy.port}\"", + "${jira.installation.path}/conf/server.xml" + ) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/LoadBalancerPlan.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/LoadBalancerPlan.kt new file mode 100644 index 00000000..fde4839e --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/loadbalancer/LoadBalancerPlan.kt @@ -0,0 +1,8 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.loadbalancer + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHooks + +interface LoadBalancerPlan { + fun materialize(nodes: List, hooks: List): HttpNode +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/network/HttpServerRoom.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/network/HttpServerRoom.kt new file mode 100644 index 00000000..7ca04eb1 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/hookapi/network/HttpServerRoom.kt @@ -0,0 +1,8 @@ +package com.atlassian.performance.tools.infrastructure.hookapi.network + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode + +interface HttpServerRoom { + + fun serveHttp(name: String): HttpNode +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraInstallation.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraInstallation.kt deleted file mode 100644 index ecf1383b..00000000 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraInstallation.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.atlassian.performance.tools.infrastructure.jira.install - -import net.jcip.annotations.ThreadSafe - -@ThreadSafe -interface JiraInstallation { - - /** - * Installs Jira on [server]. - * - * @param [server] will host the Jira - */ - fun install( - server: TcpServer - ): InstalledJira -} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TcpServer.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TcpServer.kt deleted file mode 100644 index 13cf3dd3..00000000 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TcpServer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.atlassian.performance.tools.infrastructure.jira.install - -import com.atlassian.performance.tools.ssh.api.Ssh - -/** - * Has open TCP sockets. - */ -class TcpServer( - val ip: String, - val publicPort: Int, - val privatePort: Int, - val name: String, - val ssh: Ssh -) { - constructor( - ip: String, - port: Int, - name: String, - ssh: Ssh - ) : this( - ip, - port, - port, - name, - ssh - ) -} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TomcatConfig.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TomcatConfig.kt new file mode 100644 index 00000000..dee79e44 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/TomcatConfig.kt @@ -0,0 +1,26 @@ +package com.atlassian.performance.tools.infrastructure.jira.install + +import com.atlassian.performance.tools.infrastructure.api.Sed +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class TomcatConfig( + private val jira: InstalledJira, + private val connectorPort: Int +) { + fun fixHttp( + ssh: SshConnection + ): TomcatConfig { + val httpPort = jira.http.tcp.port + if (connectorPort == httpPort) { + return this; + } + Sed().replace( + ssh, + " + hooks.call(ssh, http, reports) + } + val installed = installation.install(http, reports) + http.tcp.ssh.newConnection().use { ssh -> + hooks.postInstall.call(ssh, installed, reports) + } + return installed + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/ProfilerHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/ProfilerHook.kt new file mode 100644 index 00000000..ec6d4d93 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/ProfilerHook.kt @@ -0,0 +1,43 @@ +package com.atlassian.performance.tools.infrastructure.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHook +import com.atlassian.performance.tools.infrastructure.api.profiler.Profiler +import com.atlassian.performance.tools.infrastructure.jira.report.RemoteMonitoringProcessReport +import com.atlassian.performance.tools.ssh.api.SshConnection + +/** + * Bridges the [Profiler] SPI with the [PostInstallHook] SPI. + * In general any [Profiler] can be rewritten as a [PreStartHook] or [PostStartHook] without this bridge. + */ +class ProfilerHook( + private val profiler: Profiler +) : PostInstallHook { + override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { + profiler.install(ssh) + hooks.preStart.postStart.insert(InstalledProfiler(profiler)) + } +} + +private class InstalledProfiler( + private val profiler: Profiler +) : PostStartHook { + + override fun call( + ssh: SshConnection, + jira: StartedJira, + hooks: PostStartHooks, + reports: Reports + ) { + val process = profiler.start(ssh, jira.pid) + if (process != null) { + reports.add(RemoteMonitoringProcessReport(process), jira) + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/SplunkForwarderHook.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/SplunkForwarderHook.kt new file mode 100644 index 00000000..361872d5 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/hook/SplunkForwarderHook.kt @@ -0,0 +1,23 @@ +package com.atlassian.performance.tools.infrastructure.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PostInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.splunk.SplunkForwarder +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class SplunkForwarderHook( + private val splunk: SplunkForwarder +) : PostInstallHook { + + override fun call( + ssh: SshConnection, + jira: InstalledJira, + hooks: PostInstallHooks, + reports: Reports + ) { + splunk.jsonifyLog4j(ssh, "${jira.installation.path}/atlassian-jira/WEB-INF/classes/log4j.properties") + splunk.run(ssh, jira.http.tcp.name, "/home/ubuntu/jirahome/log") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/instance/ClusterProperties.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/instance/ClusterProperties.kt new file mode 100644 index 00000000..f0a4f310 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/instance/ClusterProperties.kt @@ -0,0 +1,13 @@ +package com.atlassian.performance.tools.infrastructure.jira.instance + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class ClusterProperties( + private val jira: InstalledJira +) { + + fun set(key: String, value: String, ssh: SshConnection) { + ssh.execute("echo '$key = $value' >> ${jira.home.path}/cluster.properties") + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/JiraLandingPage.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/JiraLandingPage.kt new file mode 100644 index 00000000..8d0b1302 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/JiraLandingPage.kt @@ -0,0 +1,19 @@ +package com.atlassian.performance.tools.infrastructure.jira.report + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Report +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class JiraLandingPage( + private val started: StartedJira +) : Report { + override fun locate(ssh: SshConnection): List { + Ubuntu().install(ssh, listOf("curl")) + val landingPage = started.installed.http.addressPrivately() + val html = "jira-landing-page.html" + val headers = "jira-landing-page-headers.txt" + ssh.execute("curl $landingPage --location --output $html --dump-header $headers --silent") + return listOf(html, headers) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/RemoteMonitoringProcessReport.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/RemoteMonitoringProcessReport.kt new file mode 100644 index 00000000..6d904047 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/report/RemoteMonitoringProcessReport.kt @@ -0,0 +1,14 @@ +package com.atlassian.performance.tools.infrastructure.jira.report + +import com.atlassian.performance.tools.infrastructure.api.jira.report.Report +import com.atlassian.performance.tools.infrastructure.api.process.RemoteMonitoringProcess +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class RemoteMonitoringProcessReport( + private val process: RemoteMonitoringProcess +) : Report { + override fun locate(ssh: SshConnection): List { + process.stop(ssh) + return listOf(process.getResultPath()) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/sharedhome/SharedHomeProperty.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/sharedhome/SharedHomeProperty.kt new file mode 100644 index 00000000..d6bda38b --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/sharedhome/SharedHomeProperty.kt @@ -0,0 +1,14 @@ +package com.atlassian.performance.tools.infrastructure.jira.sharedhome + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.jira.instance.ClusterProperties +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal class SharedHomeProperty( + private val jira: InstalledJira +) { + + fun set(mounted: String, ssh: SshConnection) { + ClusterProperties(jira).set("jira.shared.home", mounted, ssh) + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraStart.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraStart.kt deleted file mode 100644 index 9cb166bb..00000000 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/JiraStart.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.atlassian.performance.tools.infrastructure.jira.start - -import com.atlassian.performance.tools.infrastructure.jira.install.InstalledJira -import net.jcip.annotations.ThreadSafe - -@ThreadSafe -interface JiraStart { - - fun start( - installed: InstalledJira - ): StartedJira -} diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/StartedJira.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/StartedJira.kt deleted file mode 100644 index a7f29f61..00000000 --- a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/StartedJira.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.atlassian.performance.tools.infrastructure.jira.start - -import com.atlassian.performance.tools.infrastructure.jira.install.InstalledJira - -class StartedJira( - val installed: InstalledJira, - val pid: Int -) diff --git a/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/hook/HookedJiraStart.kt b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/hook/HookedJiraStart.kt new file mode 100644 index 00000000..8fc68b07 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/infrastructure/jira/start/hook/HookedJiraStart.kt @@ -0,0 +1,27 @@ +package com.atlassian.performance.tools.infrastructure.jira.start.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraStart +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PreStartHooks + +class HookedJiraStart( + private val start: JiraStart, + private val hooks: PreStartHooks +) : JiraStart { + + override fun start( + installed: InstalledJira, + reports: Reports + ): StartedJira { + installed.http.tcp.ssh.newConnection().use { ssh -> + hooks.call(ssh, installed, reports) + } + val started = start.start(installed, reports) + installed.http.tcp.ssh.newConnection().use { ssh -> + hooks.postStart.call(ssh, started, reports) + } + return started + } +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/Datasets.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/Datasets.kt new file mode 100644 index 00000000..e7bf8690 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/Datasets.kt @@ -0,0 +1,55 @@ +package com.atlassian.performance.tools.infrastructure + +import com.atlassian.performance.tools.infrastructure.hookapi.database.DockerMysqlServer +import com.atlassian.performance.tools.infrastructure.api.dataset.HttpDatasetPackage +import com.atlassian.performance.tools.infrastructure.api.jira.JiraLaunchTimeouts +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.PreInstanceHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHooks +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.RestUpgrade +import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom +import java.net.URI +import java.time.Duration + +class Datasets { + object SmallJiraEightDataset { + private val s3Bucket = URI("https://s3-eu-central-1.amazonaws.com/") + .resolve("jpt-custom-datasets-storage-a008820-datasetbucket-1nrja8d1upind/") + .resolve("dataset-a533e558-e5c5-46e7-9398-5aeda84d793a/") + + private val mysql = HttpDatasetPackage( + uri = s3Bucket.resolve("database.tar.bz2"), + downloadTimeout = Duration.ofMinutes(6) + ) + + val jiraHome = HttpDatasetPackage( + uri = s3Bucket.resolve("jirahome.tar.bz2"), + downloadTimeout = Duration.ofMinutes(6) + ) + + fun hookMysql(preInstanceHooks: PreInstanceHooks, serverRoom: TcpServerRoom) { + // encrypted with atlassian-password-encoder + val encryptedAdmin = "{PKCS5S2}dHH7Ws1DcJ1H4d9C8BN1Kh83ciEXVy025l9mIM8P3mlseybpKtI83531tOIyE/gb" + val mysqlServer = DockerMysqlServer.Builder(serverRoom, mysql) + .setPassword("admin", encryptedAdmin) + .resetCaptcha("admin") + .build() + preInstanceHooks.insert(mysqlServer) + } + + fun hookMysql(postStartHooks: PostStartHooks) { + val timeouts = JiraLaunchTimeouts.Builder() + .initTimeout(Duration.ofMinutes(4)) + .build() + val dataUpgrade = RestUpgrade(timeouts, "admin", "admin") + postStartHooks.insert(dataUpgrade) + } + + fun hookDataUpgrade(postStartHooks: PostStartHooks) { + val timeouts = JiraLaunchTimeouts.Builder() + .initTimeout(Duration.ofMinutes(4)) + .build() + val dataUpgrade = RestUpgrade(timeouts, "admin", "admin") + postStartHooks.insert(dataUpgrade) + } + } +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/DockerIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/DockerIT.kt index e0d7189f..32520f07 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/DockerIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/DockerIT.kt @@ -1,5 +1,6 @@ package com.atlassian.performance.tools.infrastructure +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.infrastructure.api.docker.DockerContainer import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.junit.Test @@ -26,32 +27,23 @@ class DockerIT { } private fun testDockerInstall(version: String) { - SshUbuntuContainer.Builder() - .enableDocker() - .version(version) - .build() - .start() - .use { ubuntu -> - ubuntu.toSsh().newConnection().use { connection -> - DockerContainer.Builder() - .imageName("hello-world") - .build() - .run(connection) - } + DockerInfrastructure(version).use { infra -> + infra.serveSsh().newConnection().use { ssh -> + DockerContainer.Builder() + .imageName("hello-world") + .build() + .run(ssh) } + } } @Test fun shouldInstallIdempotently() { - SshUbuntuContainer.Builder() - .enableDocker() - .build() - .start() - .use { ubuntu -> - ubuntu.toSsh().newConnection().use { connection -> - Docker().install(connection) - Docker().install(connection) - } + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { ssh -> + Docker().install(ssh) + Docker().install(ssh) } + } } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshExtensions.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshExtensions.kt new file mode 100644 index 00000000..201da516 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshExtensions.kt @@ -0,0 +1,9 @@ +package com.atlassian.performance.tools.infrastructure + +import com.atlassian.performance.tools.ssh.api.SshConnection + +internal fun SshConnection.SshResult.assertInterruptedJava() { + if (exitStatus !in listOf(0, 130)) { + throw Exception("$this doesn't look like an interrupted Java process") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt deleted file mode 100644 index 2de0c949..00000000 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/SshUbuntuExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.atlassian.performance.tools.infrastructure - -import com.atlassian.performance.tools.ssh.api.Ssh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntu -import java.time.Duration - -internal fun SshUbuntu.toSsh(): Ssh { - val ssh = Ssh(this.ssh) - ssh.newConnection().use { connection -> - connection.execute("apt-get update -qq", Duration.ofMinutes(3)) - connection.execute("export DEBIAN_FRONTEND=noninteractive; apt-get install sudo curl screen gnupg2 -y -qq", Duration.ofMinutes(10)) - } - return ssh -} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/DockerInfrastructure.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/DockerInfrastructure.kt new file mode 100644 index 00000000..2c13a576 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/DockerInfrastructure.kt @@ -0,0 +1,183 @@ +package com.atlassian.performance.tools.infrastructure.api + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.hookapi.network.HttpServerRoom +import com.atlassian.performance.tools.infrastructure.api.network.Networked +import com.atlassian.performance.tools.infrastructure.api.network.SshServerRoom +import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom +import com.atlassian.performance.tools.infrastructure.lib.docker.CreatedContainer +import com.atlassian.performance.tools.infrastructure.lib.docker.DockerNetwork +import com.atlassian.performance.tools.infrastructure.lib.docker.StartedContainer +import com.atlassian.performance.tools.infrastructure.lib.docker.execAsResource +import com.atlassian.performance.tools.ssh.api.Ssh +import com.atlassian.performance.tools.ssh.api.SshHost +import com.atlassian.performance.tools.ssh.api.auth.PasswordAuthentication +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.PullImageResultCallback +import com.github.dockerjava.api.model.* +import com.github.dockerjava.core.DefaultDockerClientConfig +import com.github.dockerjava.core.DockerClientImpl +import com.github.dockerjava.zerodep.ZerodepDockerHttpClient +import java.time.Duration +import java.util.* +import java.util.UUID.randomUUID +import java.util.concurrent.ConcurrentLinkedDeque + +internal class DockerInfrastructure( + private val ubuntuVersion: String = "18.04" +) : SshServerRoom, TcpServerRoom, HttpServerRoom, Networked, AutoCloseable { + + private val allocatedResources: Deque = ConcurrentLinkedDeque() + private val docker: DockerClient + private val network: DockerNetwork + private val subnetCidr: String + + init { + val dockerConfig = DefaultDockerClientConfig.createDefaultConfigBuilder().build() + val dockerHttp = ZerodepDockerHttpClient.Builder().dockerHost(dockerConfig.dockerHost).build() + docker = DockerClientImpl.getInstance(dockerConfig, dockerHttp) + allocatedResources.add(docker) + network = docker + .createNetworkCmd() + .withName(randomUUID().toString()) + .execAsResource(docker) + allocatedResources.add(network) + subnetCidr = docker + .inspectNetworkCmd() + .withNetworkId(network.response.id) + .exec() + .ipam + .config + .first() + .subnet + } + + override fun subnet(): String = subnetCidr + + fun serveSsh(): Ssh = serveSsh("ssh") + + override fun serveSsh(name: String): Ssh { + return serveTcp(888, name).ssh + } + + override fun serveTcp(name: String): TcpNode { + // TODO pre-provision all the hosts rather than on-demand - unlock batch provisioning (CFN Stack), picking EC2 types, SSD storage, TCP port ranges, subnets, etc. + return when { + name.startsWith("jira-node") -> serveTcp(8080, name) // TODO this is a contract on undocumented behavior + name.startsWith("mysql") -> serveTcp(3306, name) + name.startsWith("samba") -> serveTcp(3306, name) + else -> serveTcp(888, name) + } + } + + override fun serveHttp(name: String): HttpNode { + return HttpNode( + serveTcp(80, name), + "/", + false + ) + } + + private fun serveTcp(tcpPort: Int, name: String): TcpNode { + return serveTcp(name, listOf(tcpPort), emptyList()) + } + + + override fun serveTcp(name: String, tcpPorts: List, udpPorts: List): TcpNode { + val ports = tcpPorts.map { ExposedPort.tcp(it) } + + udpPorts.map { ExposedPort.udp(it) } + + ExposedPort.tcp(22) + val imageName = "takeyamajp/ubuntu-sshd" + val imageTag = "ubuntu$ubuntuVersion" + docker + .pullImageCmd(imageName) + .withTag(imageTag) + .exec(PullImageResultCallback()) + .awaitCompletion() + val createdContainer = docker + .createContainerCmd("$imageName:$imageTag") + .withHostConfig( + HostConfig() + .withPublishAllPorts(true) + .withPrivileged(true) +// .withBinds(Bind("/var/run/docker.sock", Volume("/var/run/docker.sock"))) +// .withMounts( +// Mount() +// .withSource("/var/run/docker.sock") +// .withTarget("/var/run/docker.sock") +// .withType(MountType.VOLUME) +// .let { listOf(it) } +// ) + .withNetworkMode(network.response.id) + ) +// .withVolumes(Volume("/var/run/docker.sock")) + .withExposedPorts(ports) + .withName("$name-${randomUUID()}") + .execAsResource(docker) + allocatedResources.addLast(createdContainer) + return start(createdContainer, tcpPorts.first(), name) + } + + private fun start( + created: CreatedContainer, + tcpPort: Int, + name: String + ): TcpNode { + val startedContainer = docker + .startContainerCmd(created.response.id) + .execAsResource(docker) + allocatedResources.addLast(startedContainer); + return install(startedContainer, tcpPort, name) + } + + private fun install( + started: StartedContainer, + tcpPort: Int, + name: String + ): TcpNode { + val networkSettings = docker + .inspectContainerCmd(started.id) + .exec() + .networkSettings + val ip = networkSettings + .networks + .values + .single { it.networkID == network.response.id } + .ipAddress!! + val sshPort = getHostPort(networkSettings, ExposedPort.tcp(22)) + val sshHost = SshHost( + ipAddress = "localhost", + userName = "root", + authentication = PasswordAuthentication("root"), + port = sshPort + ) + val ssh = Ssh(sshHost) + ssh.newConnection().use { + it.execute("apt-get update", Duration.ofMinutes(2)) + it.execute("apt-get -y install sudo gnupg screen") + } + return TcpNode("localhost", ip, tcpPort, name, ssh) + } + + private fun getHostPort( + networkSettings: NetworkSettings, + port: ExposedPort + ): Int { + return networkSettings + .ports + .bindings[port]!! + .single { it.hostIp == "0.0.0.0" } // include just the IP4 bind + .hostPortSpec + .toInt() + } + + override fun close() { + while (true) { + allocatedResources + .pollLast() + ?.use {} + ?: break + } + } +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/ChromeIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/ChromeIT.kt index c83b436f..1a656c15 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/ChromeIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/ChromeIT.kt @@ -1,8 +1,7 @@ package com.atlassian.performance.tools.infrastructure.api.browser -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.ssh.api.SshConnection -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.hamcrest.Matchers import org.junit.Assert import org.junit.Test @@ -11,8 +10,8 @@ class ChromeIT { @Test fun shouldInstallChromeBrowser() { - SshUbuntuContainer.Builder().build().start().use { ssh -> - ssh.toSsh().newConnection().use { connection -> + DockerInfrastructure().use { infra -> + infra.serveSsh("ChromeIT").newConnection().use { connection -> val wasInstalledBefore = isChromeInstalled(connection) Chrome().install(connection) @@ -23,7 +22,6 @@ class ChromeIT { Assert.assertThat(isInstalledAfter, Matchers.`is`(true)) Assert.assertThat(isChromeDriverInstalled(connection), Matchers.`is`(true)) } - } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/Chromium69IT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/Chromium69IT.kt index af1778de..02dd0f9c 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/Chromium69IT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/Chromium69IT.kt @@ -1,10 +1,9 @@ package com.atlassian.performance.tools.infrastructure.api.browser.chromium -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff import com.atlassian.performance.tools.ssh.api.SshConnection -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.hamcrest.Matchers import org.junit.Assert import org.junit.Test @@ -14,8 +13,8 @@ class Chromium69IT { @Test fun shouldInstallBrowser() { - SshUbuntuContainer.Builder().build().start().use { sshUbuntu -> - sshUbuntu.toSsh().newConnection().use { connection -> + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { connection -> val installedBefore = isChromiumInstalled(connection) Chromium69().install(connection) diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/PageLoadTimeoutRecoveryTest.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/PageLoadTimeoutRecoveryTest.kt index 960829ca..e8f952c2 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/PageLoadTimeoutRecoveryTest.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/browser/chromium/PageLoadTimeoutRecoveryTest.kt @@ -1,10 +1,9 @@ package com.atlassian.performance.tools.infrastructure.api.browser.chromium +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.infrastructure.api.browser.Browser import com.atlassian.performance.tools.infrastructure.browser.SshChromium import com.atlassian.performance.tools.infrastructure.mock.MockHttpServer -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import com.sun.net.httpserver.HttpExchange import org.assertj.core.api.Assertions import org.openqa.selenium.TimeoutException @@ -20,8 +19,8 @@ internal class PageLoadTimeoutRecoveryTest { MockHttpServer().start().use { httpServer -> val fastResource = httpServer.register(FastResponse()) val slowResource = httpServer.register(SlowResponse()) - SshUbuntuContainer.Builder().build().start().use { sshUbuntu -> - val ssh = sshUbuntu.toSsh() + DockerInfrastructure().use { infra -> + val ssh = infra.serveSsh() ssh.forwardRemotePort(httpServer.getPort(), httpServer.getPort()).use { val localChromedriverPort = findFreePort() ssh.forwardLocalPort(localChromedriverPort, remoteChromedriverPort).use { @@ -29,7 +28,7 @@ internal class PageLoadTimeoutRecoveryTest { chromium.install(connection) } val chromedriverUri = URI("http://localhost:$localChromedriverPort") - SshChromium(ssh.newConnection(), chromedriverUri).start().use { sshDriver -> + SshChromium(ssh, chromedriverUri).start().use { sshDriver -> val driver = sshDriver.getDriver() setPageLoadTimeout(driver) @@ -47,7 +46,7 @@ internal class PageLoadTimeoutRecoveryTest { } private fun findFreePort(): Int { - return ServerSocket(0).use { socket -> return socket.localPort } + return ServerSocket(0).use { socket -> socket.localPort } } private class FastResponse : MockHttpServer.RequestHandler { diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/LicenseOverridingMysqlTest.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/LicenseOverridingMysqlTest.kt index b03f9f7a..69468123 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/LicenseOverridingMysqlTest.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/LicenseOverridingMysqlTest.kt @@ -113,13 +113,13 @@ class LicenseOverridingMysqlTest { private fun generateExpectedCommands(ssh: RememberingSshConnection): List { return listOf( // essentially, delete all existing licences - """mysql -h 127.0.0.1 -u root -e "DELETE FROM jiradb.productlicense;"""", + """mysql -h 127.0.0.1 -P 3306 -u root -e "DELETE FROM jiradb.productlicense;"""", // then import the new ones *ssh.uploads .map { listOf( - """mysql -h 127.0.0.1 -u root < ${it.remoteDestination}""", + """mysql -h 127.0.0.1 -P 3306 -u root < ${it.remoteDestination}""", """rm ${it.remoteDestination}""" ) } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabaseIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabaseIT.kt index a50a8bce..0c082194 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabaseIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MinimalMySqlDatabaseIT.kt @@ -1,10 +1,8 @@ package com.atlassian.performance.tools.infrastructure.api.database -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import org.junit.Test import java.net.URI -import java.util.function.Consumer class MinimalMysqlDatabaseIT { @Test @@ -15,17 +13,13 @@ class MinimalMysqlDatabaseIT { .jiraDbUserPassword(password) .build() - SshUbuntuContainer.Builder() - .customization(Consumer { it.setPrivilegedMode(true) }) - .build() - .start() - .use { ubuntu -> - ubuntu.toSsh().newConnection().use { ssh -> - mysql.setup(ssh) - mysql.start(URI("https://dummy-jira.net"), ssh) + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { ssh-> + mysql.setup(ssh) + mysql.start(URI("https://dummy-jira.net"), ssh) - ssh.execute("mysql -h 127.0.0.1 -u $assumedUser -p$password -e 'USE jiradb; SHOW TABLES;'") - } + ssh.execute("mysql -h 127.0.0.1 -u $assumedUser -p$password -e 'USE jiradb; SHOW TABLES;'") } + } } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabaseIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabaseIT.kt index db3485eb..4b65fa23 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabaseIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/database/MySqlDatabaseIT.kt @@ -1,12 +1,10 @@ package com.atlassian.performance.tools.infrastructure.api.database +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.infrastructure.api.dataset.HttpDatasetPackage -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.junit.Test import java.net.URI import java.time.Duration -import java.util.function.Consumer /** * Note: We decided to use `Mysql` casing in this repository. Please don't repeat `MySql`. @@ -23,15 +21,11 @@ class MySqlDatabaseIT { ) val mysql = MySqlDatabase(databaseSource) - SshUbuntuContainer.Builder() - .customization(Consumer { it.setPrivilegedMode(true) }) - .build() - .start() - .use { ubuntu -> - ubuntu.toSsh().newConnection().use { ssh -> - mysql.setup(ssh) - mysql.start(URI("https://dummy-jira.net"), ssh) - } + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { ssh -> + mysql.setup(ssh) + mysql.start(URI("https://dummy-jira.net"), ssh) } + } } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/dataset/HttpDatasetPackageIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/dataset/HttpDatasetPackageIT.kt index 12a7f0ba..0281cb58 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/dataset/HttpDatasetPackageIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/dataset/HttpDatasetPackageIT.kt @@ -1,9 +1,8 @@ package com.atlassian.performance.tools.infrastructure.api.dataset import com.atlassian.performance.tools.infrastructure.Ls -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.ssh.api.Ssh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions import org.junit.Test import java.net.URI @@ -21,8 +20,8 @@ class HttpDatasetPackageIT { downloadTimeout = Duration.ofMinutes(1) ) - val filesInDataset = SshUbuntuContainer.Builder().build().start().use { sshUbuntu -> - val ssh = sshUbuntu.toSsh() + val filesInDataset = DockerInfrastructure().use { infra -> + val ssh = infra.serveSsh("HttpDatasetPackageIT") return@use RandomFilesGenerator(ssh).start().use { ssh.newConnection().use { connection -> val unpackedPath = dataset.download(connection) diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraServiceDeskDistributionIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraServiceDeskDistributionIT.kt index 4ee1a021..4635244c 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraServiceDeskDistributionIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraServiceDeskDistributionIT.kt @@ -1,7 +1,6 @@ package com.atlassian.performance.tools.infrastructure.api.distribution -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -9,9 +8,11 @@ class PublicJiraServiceDeskDistributionIT { @Test fun shouldDownloadJiraServiceDesk() { - SshUbuntuContainer.Builder().build().start().use { ssh -> - ssh.toSsh().newConnection().use { connection -> - val distro = PublicJiraServiceDeskDistribution("4.0.1") + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { connection -> + val distro: ProductDistribution = PublicJiraServiceDeskDistribution("4.0.1") + val targetFolder = "test" + connection.execute("mkdir $targetFolder") val installation = distro.install(connection, "destination") diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraSoftwareDistributionsIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraSoftwareDistributionsIT.kt index 399259a6..88210083 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraSoftwareDistributionsIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/distribution/PublicJiraSoftwareDistributionsIT.kt @@ -1,10 +1,9 @@ package com.atlassian.performance.tools.infrastructure.api.distribution +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.infrastructure.api.jira.JiraGcLog import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig import com.atlassian.performance.tools.infrastructure.api.jira.SetenvSh -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -12,9 +11,11 @@ class PublicJiraSoftwareDistributionsIT { @Test fun shouldDownloadJiraSoftware() { - SshUbuntuContainer.Builder().build().start().use { ssh -> - ssh.toSsh().newConnection().use { connection -> - val distro = PublicJiraSoftwareDistribution("7.2.0") + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { connection -> + val distro: ProductDistribution = PublicJiraSoftwareDistribution("7.2.0") + val targetFolder = "test" + connection.execute("mkdir $targetFolder") val installation = distro.install(connection, "destination") @@ -26,8 +27,8 @@ class PublicJiraSoftwareDistributionsIT { @Test fun shouldInstallReleaseCandidate() { - SshUbuntuContainer.Builder().build().start().use { ssh -> - ssh.toSsh().newConnection().use { connection -> + DockerInfrastructure().use { infra -> + infra.serveSsh().newConnection().use { connection -> val distro = PublicJiraSoftwareDistribution("9.0.0-RC01") val installation = distro.install(connection, "destination") diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt new file mode 100644 index 00000000..83a6148f --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/install/hook/PreInstallHooksTest.kt @@ -0,0 +1,69 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.install.hook + +import com.atlassian.performance.tools.infrastructure.api.jira.install.HttpNode +import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpNode +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.mock.UnimplementedSshConnection +import com.atlassian.performance.tools.ssh.api.Ssh +import com.atlassian.performance.tools.ssh.api.SshConnection +import com.atlassian.performance.tools.ssh.api.SshHost +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths + +class PreInstallHooksTest { + + private val dummySsh = Ssh(SshHost("localhost", "dummyUser", Paths.get("dummyKey"))) + private val dummyHttp = HttpNode( + TcpNode("dummyPublicIp", "dummyPrivateIp", 123, "fake-server", dummySsh), + "/", + false + ) + + @Test + fun shouldInsertDuringListing() { + val counter = CountingHook() + val hooks = PreInstallHooks.empty().apply { + insert(counter) + insert(InsertingHook(counter)) + insert(counter) + } + + hooks.call(UnimplementedSshConnection(), dummyHttp, Reports()) + + assertThat(counter.count).isEqualTo(3) + } + + @Test + fun shouldHookToTheTailDuringListing() { + val counter = CountingHook() + val hooks = PreInstallHooks.empty().apply { + insert(counter) + insert(counter) + insert(InsertingHook(counter)) + } + + hooks.call(UnimplementedSshConnection(), dummyHttp, Reports()) + + assertThat(counter.count).isEqualTo(3) + } +} + +private class CountingHook : PreInstallHook { + + var count = 0 + + override fun call(ssh: SshConnection, http: HttpNode, hooks: PreInstallHooks, reports: Reports) { + count++ + } +} + +private class InsertingHook( + private val hook: PreInstallHook +) : PreInstallHook { + override fun call(ssh: SshConnection, http: HttpNode, hooks: PreInstallHooks, reports: Reports) { + hooks.insert(hook) + } +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/instance/JiraPlanIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/instance/JiraPlanIT.kt new file mode 100644 index 00000000..17524180 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jira/instance/JiraPlanIT.kt @@ -0,0 +1,246 @@ +package com.atlassian.performance.tools.infrastructure.api.jira.instance + +import com.atlassian.performance.tools.infrastructure.Datasets +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure +import com.atlassian.performance.tools.infrastructure.api.distribution.PublicJiraSoftwareDistribution +import com.atlassian.performance.tools.infrastructure.api.jira.JiraHomePackage +import com.atlassian.performance.tools.infrastructure.api.jira.install.ParallelInstallation +import com.atlassian.performance.tools.infrastructure.hookapi.jira.install.hook.PreInstallHooks +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.hookapi.jira.sharedhome.SambaSharedHome +import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraLaunchScript +import com.atlassian.performance.tools.infrastructure.api.jira.start.StartedJira +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHook +import com.atlassian.performance.tools.infrastructure.hookapi.jira.start.hook.PostStartHooks +import com.atlassian.performance.tools.infrastructure.api.jvm.AdoptOpenJDK +import com.atlassian.performance.tools.infrastructure.hookapi.loadbalancer.ApacheProxyPlan +import com.atlassian.performance.tools.infrastructure.hookapi.jira.instance.* +import com.atlassian.performance.tools.io.api.resolveSafely +import com.atlassian.performance.tools.ssh.api.SshConnection +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchThrowable +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Paths +import java.time.Duration +import java.time.Instant + +class JiraPlanIT { + + private lateinit var infrastructure: DockerInfrastructure + private val dataset = Datasets.SmallJiraEightDataset + private val jiraVersion = "9.4.9" + private val jiraDistribution = PublicJiraSoftwareDistribution(jiraVersion) + + @Before + fun setUp() { + infrastructure = DockerInfrastructure() + } + + @After + fun tearDown() { + infrastructure.close() + } + + @Test + fun shouldStartDataCenter() { + // given + val jiraHomeSource = JiraHomePackage(dataset.jiraHome) + val nodePlans = listOf(1, 2).map { + val nodeHooks = PreInstallHooks.default() + .also { dataset.hookMysql(it.postStart) } + JiraNodePlan.Builder(infrastructure) + .installation( + ParallelInstallation( + jiraHomeSource = jiraHomeSource, + productDistribution = jiraDistribution, + jdk = AdoptOpenJDK() + ) + ) + .start(JiraLaunchScript()) + .hooks(nodeHooks) + .build() + } + val instanceHooks = PreInstanceHooks.default() + .also { dataset.hookMysql(it, infrastructure) } + .also { it.insert(SambaSharedHome(jiraHomeSource, infrastructure)) } + val dcPlan = JiraDataCenterPlan.Builder(infrastructure) + .nodePlans(nodePlans) + .instanceHooks(instanceHooks) + .balancerPlan(ApacheProxyPlan(infrastructure)) + .build() + + // when + val dataCenter = dcPlan.materialize() + + // then + assertJiraAccessible(dataCenter) + dataCenter.nodes.forEach { node -> + val installed = node.installed + val serverXml = installed + .installation + .resolve("conf/server.xml") + .download(Files.createTempFile("downloaded-config", ".xml")) + assertThat(serverXml.readText()).contains(" + ssh.execute("wget ${jira.address.addressPrivately()}") + } + try { + val response = jira.address.addressPublicly().toURL().readText() + assertThat(response).contains(" - val ssh = ubuntu.toSsh() + DockerInfrastructure().use { infra -> + val ssh = infra.serveSsh() ssh.newConnection().use { connection -> jdk.install(connection) val javaHomeOutput = connection.execute("source ~/.profile; echo \$JAVA_HOME").output diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JstatSupport.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JstatSupport.kt index a4f0f52f..8498718a 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JstatSupport.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/JstatSupport.kt @@ -1,38 +1,27 @@ package com.atlassian.performance.tools.infrastructure.api.jvm -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure +import com.atlassian.performance.tools.infrastructure.api.os.Ubuntu +import com.atlassian.performance.tools.infrastructure.assertInterruptedJava import com.atlassian.performance.tools.jvmtasks.api.ExponentialBackoff import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.ssh.api.Ssh import com.atlassian.performance.tools.ssh.api.SshConnection -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions.assertThat import java.io.File import java.time.Duration +import java.time.Duration.ofSeconds class JstatSupport( private val jdk: VersionedJavaDevelopmentKit ) { - private val expectedStats: Set = setOf( - "Timestamp", - "S0", - "S1", - "E", - "O", - "M", - "CCS", - "YGC", - "YGCT", - "FGC", - "FGCT", - "GCT" - ) private val jarName = "hello-world-after-1m-wait.jar" - private val jar = "/com/atlassian/performance/tools/infrastructure/api/jvm/$jarName" + private val jarResource = "/com/atlassian/performance/tools/infrastructure/api/jvm/$jarName" + private val timestampLength = "2018-12-17T14:10:44+00:00 ".length fun shouldSupportJstat() { - SshUbuntuContainer.Builder().build().start().use { ubuntu -> - val ssh = ubuntu.toSsh() + DockerInfrastructure().use { infra -> + val ssh = infra.serveSsh() ssh.newConnection().use { connection -> shouldSupportJstat(ssh, connection) } @@ -40,24 +29,29 @@ class JstatSupport( } private fun shouldSupportJstat(ssh: Ssh, connection: SshConnection) { - connection.upload(File(this.javaClass.getResource(jar).toURI()), jarName) + Ubuntu().install(connection, listOf("screen"), Duration.ofMinutes(2)) + connection.upload(File(javaClass.getResource(jarResource)!!.toURI()), jarName) jdk.install(connection) - ssh.runInBackground(jdk.command("-classpath $jarName samples.HelloWorld")) + val hello = ssh.runInBackground(jdk.command("-classpath $jarName samples.HelloWorld")) val pid = IdempotentAction("Wait for the Hello, World! process to start.") { connection.execute("cat hello-world.pid").output }.retry( maxAttempts = 3, - backoff = ExponentialBackoff(Duration.ofSeconds(1)) + backoff = ExponentialBackoff(ofSeconds(1)) ) val jstatMonitoring = jdk.jstatMonitoring.start(connection, pid.toInt()) waitForJstatToCollectSomeData() jstatMonitoring.stop(connection) + hello.stop(ofSeconds(1)).assertInterruptedJava(); val jstatLog = connection.execute("head -n 1 ${jstatMonitoring.getResultPath()}").output + val jstatHeader = jstatLog.substring(timestampLength, jstatLog.indexOf('\n')) - assertThat(jstatLog).contains(this.expectedStats) + assertThat(jstatHeader).contains( + "Timestamp", "S0", "S1", "E", "O", "M", "CCS", "YGC", "YGCT", "FGC", "FGCT", "GCT" + ) } private fun waitForJstatToCollectSomeData() { - Thread.sleep(8 * 1000) + Thread.sleep(ofSeconds(8).toMillis()) } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDumpIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDumpIT.kt index 84e980a7..09bbb8a2 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDumpIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/jvm/ThreadDumpIT.kt @@ -1,11 +1,11 @@ package com.atlassian.performance.tools.infrastructure.api.jvm -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure +import com.atlassian.performance.tools.infrastructure.assertInterruptedJava import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff import com.atlassian.performance.tools.ssh.api.Ssh import com.atlassian.performance.tools.ssh.api.SshConnection -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions.assertThat import java.time.Duration @@ -13,8 +13,8 @@ class ThreadDumpTest( private val jdk: JavaDevelopmentKit ) { fun shouldGatherThreadDump() { - SshUbuntuContainer.Builder().build().start().use { ubuntu -> - val ssh = ubuntu.toSsh() + DockerInfrastructure().use { infra -> + val ssh = infra.serveSsh() ssh.newConnection().use { connection -> shouldGatherThreadDump(ssh, connection) } @@ -26,7 +26,8 @@ class ThreadDumpTest( val destination = "thread-dumps" connection.execute("""echo "public class Test { public static void main(String[] args) { try { Thread.sleep(java.time.Duration.ofMinutes(1).toMillis()); } catch (InterruptedException e) { throw new RuntimeException(e); } }}" > Test.java """.trimIndent()) connection.execute("${jdk.use()}; javac Test.java") - ssh.runInBackground("${jdk.use()}; java Test").use { + val process = ssh.runInBackground("${jdk.use()}; java Test") + try { val pid = IdempotentAction("Get PID") { getPid(connection, jdk) }.retry(maxAttempts = 2, backoff = StaticBackoff(Duration.ofSeconds(1))) @@ -36,6 +37,8 @@ class ThreadDumpTest( val threadDumpFile = connection.execute("ls $destination").output val threadDump = connection.execute("cat $destination/$threadDumpFile").output assertThat(threadDump).contains("Full thread dump Java HotSpot") + } finally { + process.stop(Duration.ofSeconds(1)).assertInterruptedJava() } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/os/UbuntuIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/os/UbuntuIT.kt index 662d872c..ed019fa2 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/os/UbuntuIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/os/UbuntuIT.kt @@ -1,10 +1,9 @@ package com.atlassian.performance.tools.infrastructure.api.os -import com.atlassian.performance.tools.infrastructure.toSsh +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure +import com.atlassian.performance.tools.ssh.api.Ssh import com.atlassian.performance.tools.ssh.api.SshConnection import com.atlassian.performance.tools.ssh.api.SshHost -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntu -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.apache.logging.log4j.Level import org.junit.After import org.junit.Before @@ -14,23 +13,25 @@ import java.util.concurrent.* class UbuntuIT { private lateinit var executor: ExecutorService - private lateinit var sshUbuntu: SshUbuntu + private lateinit var infra: DockerInfrastructure + private lateinit var ssh: Ssh @Before fun before() { executor = Executors.newCachedThreadPool() - sshUbuntu = SshUbuntuContainer.Builder().build().start() + infra = DockerInfrastructure() + ssh = infra.serveSsh("UbuntuIT") } @After fun after() { - sshUbuntu.close() + infra.close() executor.shutdownNow() } @Test fun shouldRetry() { - sshUbuntu.toSsh().newConnection().use { connection -> + ssh.newConnection().use { connection -> Ubuntu().install( ColdAptSshConnection(connection), listOf("nano"), @@ -80,7 +81,6 @@ class UbuntuIT { fun shouldBeThreadSafe() { val completion = ExecutorCompletionService(executor) val readyToUseUbuntu = CountDownLatch(1) - val ssh = sshUbuntu.toSsh() val installations = List(5) { Callable { diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt index f3042d1f..0a2fa4e7 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/api/profiler/AsyncProfilerIT.kt @@ -1,19 +1,17 @@ package com.atlassian.performance.tools.infrastructure.api.profiler +import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure import com.atlassian.performance.tools.infrastructure.api.distribution.PublicJiraSoftwareDistribution import com.atlassian.performance.tools.infrastructure.api.jira.EmptyJiraHome +import com.atlassian.performance.tools.infrastructure.api.jira.install.InstalledJira +import com.atlassian.performance.tools.infrastructure.api.jira.install.ParallelInstallation +import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports +import com.atlassian.performance.tools.infrastructure.api.jira.start.JiraLaunchScript import com.atlassian.performance.tools.infrastructure.api.jvm.AdoptOpenJDK import com.atlassian.performance.tools.infrastructure.api.os.RemotePath -import com.atlassian.performance.tools.infrastructure.jira.install.InstalledJira -import com.atlassian.performance.tools.infrastructure.jira.install.ParallelInstallation -import com.atlassian.performance.tools.infrastructure.jira.install.TcpServer -import com.atlassian.performance.tools.infrastructure.jira.start.JiraLaunchScript -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Files.createTempFile -import java.util.function.Consumer class AsyncProfilerIT { @@ -32,17 +30,16 @@ class AsyncProfilerIT { val profiler = AsyncProfiler() testOnInstalledJira(ubuntuVersion) { installedJira -> - val sshClient = installedJira.server.ssh - sshClient.newConnection().use { ssh -> + installedJira.http.tcp.ssh.newConnection().use { ssh -> // when profiler.install(ssh) - val startedJira = JiraLaunchScript().start(installedJira) + val startedJira = JiraLaunchScript().start(installedJira, Reports()) val process = profiler.start(ssh, startedJira.pid) Thread.sleep(5000) process.stop(ssh) // then - val profilerResult = RemotePath(sshClient.host, process.getResultPath()) + val profilerResult = RemotePath(installedJira.http.tcp.ssh.host, process.getResultPath()) .download(createTempFile("profiler-result", ".svg")) assertThat(profilerResult.readLines().take(5)).contains("") } @@ -50,27 +47,13 @@ class AsyncProfilerIT { } private fun testOnInstalledJira(ubuntuVersion: String, test: (InstalledJira) -> T) { - val privatePort = 8080 - val container = SshUbuntuContainer.Builder() - .version(ubuntuVersion) - .customization(Consumer { - it.addExposedPort(privatePort) - it.setPrivilegedMode(true) - }) - .build() - container.start().use { sshUbuntu -> - val server = TcpServer( - "localhost", - sshUbuntu.container.getMappedPort(privatePort), - privatePort, - "my-jira", - sshUbuntu.toSsh() - ) + DockerInfrastructure(ubuntuVersion).use { infra -> + val jiraNode = infra.serveHttp("jira") val installed = ParallelInstallation( jiraHomeSource = EmptyJiraHome(), productDistribution = PublicJiraSoftwareDistribution("7.13.0"), jdk = AdoptOpenJDK() - ).install(server) + ).install(jiraNode, Reports()) test(installed) } } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/browser/SshChromium.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/browser/SshChromium.kt index 81428ebf..89034461 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/browser/SshChromium.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/browser/SshChromium.kt @@ -2,22 +2,21 @@ package com.atlassian.performance.tools.infrastructure.browser import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction import com.atlassian.performance.tools.jvmtasks.api.StaticBackoff -import com.atlassian.performance.tools.ssh.api.DetachedProcess +import com.atlassian.performance.tools.ssh.api.Ssh import com.atlassian.performance.tools.ssh.api.SshConnection import com.atlassian.performance.tools.virtualusers.api.browsers.Browser import com.atlassian.performance.tools.virtualusers.api.browsers.CloseableRemoteWebDriver import org.openqa.selenium.chrome.ChromeOptions import org.openqa.selenium.remote.RemoteWebDriver import java.net.URI -import java.time.Duration +import java.time.Duration.ofSeconds internal class SshChromium( - private val ssh: SshConnection, + private val ssh: Ssh, private val chromedriverUri: URI ) : Browser { override fun start(): CloseableRemoteWebDriver { - val chromedriverProcess: DetachedProcess = ssh.startProcess("chromedriver --whitelisted-ips") - + ssh.runInBackground("chromedriver --whitelisted-ips") val chromeOptions = ChromeOptions() .apply { addArguments("--headless") } .apply { addArguments("--no-sandbox") } @@ -28,16 +27,13 @@ internal class SshChromium( "credentials_enable_service" to false ) ) - IdempotentAction("Wait for chrome process") { - waitForChromeProcess(ssh) - }.retry(maxAttempts = 3, backoff = StaticBackoff(Duration.ofSeconds(5L))) - val driver = RemoteWebDriver(chromedriverUri.toURL(), chromeOptions) - return object : CloseableRemoteWebDriver(driver) { - override fun close() { - super.close() - ssh.stopProcess(chromedriverProcess) - } + ssh.newConnection().use { connection -> + IdempotentAction("Wait for chrome process") { + waitForChromeProcess(connection) + }.retry(3, StaticBackoff(ofSeconds(5))) } + val driver = RemoteWebDriver(chromedriverUri.toURL(), chromeOptions) + return CloseableRemoteWebDriver(driver) } private fun waitForChromeProcess(ssh: SshConnection) { diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClientTest.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClientTest.kt index 2773e309..92c9261d 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClientTest.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/database/SshMysqlClientTest.kt @@ -20,7 +20,7 @@ class SshMysqlClientTest { assertThat(ssh.commands) .`as`("SSH commands") .containsExactly( - "mysql -h 127.0.0.1 -u root -e \"$command\"" + "mysql -h 127.0.0.1 -P 3306 -u root -e \"$command\"" ) } @@ -36,7 +36,7 @@ class SshMysqlClientTest { assertThat(ssh.commands) .`as`("SSH commands") .containsExactly( - "mysql -h 127.0.0.1 -u root < ${file.name}", + "mysql -h 127.0.0.1 -P 3306 -u root < ${file.name}", "rm ${file.name}" ) } diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraLaunchScriptIT.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraLaunchScriptIT.kt deleted file mode 100644 index 77c70105..00000000 --- a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/jira/install/JiraLaunchScriptIT.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.atlassian.performance.tools.infrastructure.jira.install - -import com.atlassian.performance.tools.infrastructure.api.distribution.PublicJiraSoftwareDistribution -import com.atlassian.performance.tools.infrastructure.api.jira.EmptyJiraHome -import com.atlassian.performance.tools.infrastructure.api.jvm.AdoptOpenJDK -import com.atlassian.performance.tools.infrastructure.jira.start.JiraLaunchScript -import com.atlassian.performance.tools.infrastructure.toSsh -import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test -import java.nio.file.Files -import java.util.function.Consumer - -class JiraLaunchScriptIT { - - @Test - fun shouldInstallJira() { - // given - val installation = ParallelInstallation( - jiraHomeSource = EmptyJiraHome(), - productDistribution = PublicJiraSoftwareDistribution("7.13.0"), - jdk = AdoptOpenJDK() - ) - val start = JiraLaunchScript() - - testOnServer { server -> - // when - val installed = installation.install(server) - val started = start.start(installed) - - // then - val serverXml = installed - .installation - .resolve("conf/server.xml") - .download(Files.createTempFile("downloaded-config", ".xml")) - assertThat(serverXml.readText()).contains(" testOnServer(test: (TcpServer) -> T) { - val privatePort = 8080 - val container = SshUbuntuContainer.Builder() - .customization(Consumer { it.addExposedPort(privatePort) }) - .build() - container.start().use { sshUbuntu -> - val server = TcpServer( - "localhost", - sshUbuntu.container.getMappedPort(privatePort), - privatePort, - "my-jira", - sshUbuntu.toSsh() - ) - test(server) - } - } -} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/ConnectedContainer.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/ConnectedContainer.kt new file mode 100644 index 00000000..ee5ae626 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/ConnectedContainer.kt @@ -0,0 +1,30 @@ +package com.atlassian.performance.tools.infrastructure.lib.docker + +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.ConnectToNetworkCmd + +class ConnectedContainer( + private val docker: DockerClient, + val containerId: String, + val networkId: String +) : AutoCloseable { + + override fun close() { + docker + .disconnectFromNetworkCmd() + .withContainerId(containerId) + .withNetworkId(networkId) + .exec() + } +} + +fun ConnectToNetworkCmd.execAsResource( + docker: DockerClient +): ConnectedContainer { + exec() + return ConnectedContainer( + docker, + containerId!!, + networkId!! + ) +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/CreatedContainer.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/CreatedContainer.kt new file mode 100644 index 00000000..b06863ec --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/CreatedContainer.kt @@ -0,0 +1,18 @@ +package com.atlassian.performance.tools.infrastructure.lib.docker + +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.CreateContainerCmd +import com.github.dockerjava.api.command.CreateContainerResponse + +class CreatedContainer( + private val docker: DockerClient, + val response: CreateContainerResponse +) : AutoCloseable { + override fun close() { + docker.removeContainerCmd(response.id).exec() + } +} + +fun CreateContainerCmd.execAsResource( + docker: DockerClient +): CreatedContainer = CreatedContainer(docker, exec()) diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/DockerNetwork.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/DockerNetwork.kt new file mode 100644 index 00000000..c137d153 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/DockerNetwork.kt @@ -0,0 +1,18 @@ +package com.atlassian.performance.tools.infrastructure.lib.docker + +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.CreateNetworkCmd +import com.github.dockerjava.api.command.CreateNetworkResponse + +class DockerNetwork( + private val docker: DockerClient, + val response: CreateNetworkResponse +) : AutoCloseable { + override fun close() { + docker.removeNetworkCmd(response.id).exec() + } +} + +fun CreateNetworkCmd.execAsResource( + docker: DockerClient +): DockerNetwork = DockerNetwork(docker, exec()) \ No newline at end of file diff --git a/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/StartedContainer.kt b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/StartedContainer.kt new file mode 100644 index 00000000..076a02a7 --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/infrastructure/lib/docker/StartedContainer.kt @@ -0,0 +1,23 @@ +package com.atlassian.performance.tools.infrastructure.lib.docker + +import com.github.dockerjava.api.DockerClient +import com.github.dockerjava.api.command.StartContainerCmd + +class StartedContainer( + private val docker: DockerClient, + val id: String +) : AutoCloseable { + override fun close() { + docker.stopContainerCmd(id).exec() + } +} + +fun StartContainerCmd.execAsResource( + docker: DockerClient +): StartedContainer { + exec() + return StartedContainer( + docker, + containerId!! + ) +}