Skip to content

Commit

Permalink
JCES-273: Add load balancers to Data Center
Browse files Browse the repository at this point in the history
  • Loading branch information
dagguh committed Apr 23, 2021
1 parent 2d4d8e8 commit 59b144e
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.atlassian.performance.tools.infrastructure.api

import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpHost
import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNode
import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNodePlan

interface Infrastructure : AutoCloseable {
fun serve(jiraNodePlans: List<JiraNodePlan>): List<JiraNode>

/**
* @return can be reached by the caller via [TcpHost.publicIp] and by the rest of the infra via [TcpHost.privateIp]
*/
fun serve(port: Int, name: String): TcpHost
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@ import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNodePlan
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.loadbalancer.LoadBalancer
import com.atlassian.performance.tools.infrastructure.api.loadbalancer.LoadBalancerPlan
import java.net.URI
import java.time.Duration
import java.util.function.Supplier
import kotlin.streams.asStream
import kotlin.streams.toList

class JiraDataCenterPlan constructor(
val nodePlans: List<JiraNodePlan>,
val instanceHooks: PreInstanceHooks,
val loadBalancerSupplier: Supplier<LoadBalancer>,
val infrastructure: Infrastructure
private val nodePlans: List<JiraNodePlan>,
private val instanceHooks: PreInstanceHooks,
private val balancerPlan: LoadBalancerPlan,
private val infrastructure: Infrastructure
) : JiraInstancePlan {

private val reports: Reports = Reports()
private val loadBalancingPatience = Duration.ofMinutes(5)

override fun materialize(): JiraInstance {
instanceHooks.call(nodePlans.map { it.hooks }, reports)
val nodes = infrastructure.serve(nodePlans)
val nodes = nodePlans.mapIndexed { nodeIndex, nodePlan ->
val nodeNumber = nodeIndex + 1
val host = infrastructure.serve(8080, "jira-node-$nodeNumber")
nodePlan.materialize(host)
}
val balancer = balancerPlan.materialize(nodes)
val installed = installInParallel(nodes)
val started = installed.map { it.start(reports) }
val loadBalancer = loadBalancerSupplier.get()
val instance = JiraDataCenter(started, loadBalancer)
val instance = JiraDataCenter(started, balancer)
instanceHooks.postInstance.call(instance, reports)
loadBalancer.waitUntilHealthy(loadBalancingPatience)
balancer.waitUntilHealthy(loadBalancingPatience)
return instance
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.atlassian.performance.tools.infrastructure.api.loadbalancer

import com.atlassian.performance.tools.infrastructure.api.Infrastructure
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.node.JiraNode
import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports
import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PreStartHook
import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PreStartHooks
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
import java.net.URI
import java.time.Duration

class ApacheProxyPlan(
private val httpPort: Int,
private val infrastructure: Infrastructure
) : LoadBalancerPlan {

private val configPath = "/etc/apache2/sites-enabled/000-default.conf"

override fun materialize(nodes: List<JiraNode>): LoadBalancer {
val proxyNode = infrastructure.serve(httpPort, "apache-proxy")
IdempotentAction("Installing and configuring apache load balancer") {
proxyNode.ssh.newConnection().use { connection ->
tryToProvision(connection, nodes)
}
}.retry(2, ExponentialBackoff(Duration.ofSeconds(5)))
val balancerEndpoint = URI("http://${proxyNode.privateIp}:$httpPort/")
nodes.forEach { it.plan.hooks.preStart.insert(InjectProxy(balancerEndpoint)) }
return ApacheProxy(balancerEndpoint)
}

private fun tryToProvision(ssh: SshConnection, nodes: List<JiraNode>) {
Ubuntu().install(ssh, listOf("apache2"))
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, "<Proxy balancer://mycluster>")
nodes.forEachIndexed { index, node ->
appendConfig(ssh, "\tBalancerMember http://${node.host.publicIp}:${node.host.port} route=$index")
}
appendConfig(ssh, "</Proxy>\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 ApacheProxy(
override val uri: URI
) : LoadBalancer {
override fun waitUntilHealthy(timeout: Duration) {}
}

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"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.atlassian.performance.tools.infrastructure.api.loadbalancer

import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNode

interface LoadBalancerPlan {
fun materialize(nodes: List<JiraNode>): LoadBalancer
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.atlassian.performance.tools.infrastructure.api

import com.atlassian.performance.tools.infrastructure.api.jira.install.TcpHost
import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNode
import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNodePlan
import com.atlassian.performance.tools.infrastructure.lib.docker.*
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.api.model.ExposedPort
import com.github.dockerjava.api.model.HostConfig
import com.github.dockerjava.api.model.NetworkSettings
import com.github.dockerjava.core.DefaultDockerClientConfig
import com.github.dockerjava.core.DockerClientImpl
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient
Expand All @@ -36,13 +39,6 @@ internal class DockerInfrastructure : Infrastructure {
allocatedResources.add(network)
}

override fun serve(jiraNodePlans: List<JiraNodePlan>): List<JiraNode> {
return jiraNodePlans.mapIndexed { nodeIndex, plan ->
val nodeNumber = nodeIndex + 1
plan.materialize(serve(8080, "jira-node-$nodeNumber"))
}
}

fun serve(): Ssh {
return serve("ssh")
}
Expand All @@ -63,10 +59,6 @@ internal class DockerInfrastructure : Infrastructure {
.withHostConfig(
HostConfig()
.withPublishAllPorts(true)
// .withPortBindings(
// PortBinding(Ports.Binding("0.0.0.0", "0"), ExposedPort.tcp(22)),
// PortBinding(Ports.Binding("0.0.0.0", "0"), exposedPort)
// )
.withPrivileged(true)
.withNetworkMode(network.response.id)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ import com.atlassian.performance.tools.infrastructure.api.DockerInfrastructure
import com.atlassian.performance.tools.infrastructure.api.Infrastructure
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.InstalledJira
import com.atlassian.performance.tools.infrastructure.api.jira.install.ParallelInstallation
import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHooks
import com.atlassian.performance.tools.infrastructure.api.jira.node.JiraNodePlan
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.jira.start.hook.PreStartHook
import com.atlassian.performance.tools.infrastructure.api.jira.start.hook.PreStartHooks
import com.atlassian.performance.tools.infrastructure.api.jvm.AdoptOpenJDK
import com.atlassian.performance.tools.infrastructure.api.loadbalancer.ApacheProxyPlan
import com.atlassian.performance.tools.ssh.api.SshConnection
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.lang.Exception
import java.nio.file.Files
import java.util.function.Supplier

class JiraDataCenterPlanIT {

Expand Down Expand Up @@ -48,9 +54,9 @@ class JiraDataCenterPlanIT {
.build()
}
val instanceHooks = PreInstanceHooks.default()
// TODO this plus `EmptyJiraHome()` = failing `DatabaseIpConfig` - couple them together or stop expecting a preexisting `dbconfig.xml`? but then what about missing lucene indexes?
.also { Datasets.JiraSevenDataset.hookMysql(it, infrastructure) }
val dcPlan = JiraDataCenterPlan(nodePlans, instanceHooks, Supplier { TODO() }, infrastructure)
val balancerPlan = ApacheProxyPlan(80, infrastructure)
val dcPlan = JiraDataCenterPlan(nodePlans, instanceHooks, balancerPlan, infrastructure)

// when
val dataCenter = dcPlan.materialize()
Expand All @@ -71,6 +77,12 @@ class JiraDataCenterPlanIT {
@Test
fun shouldProvideLogsToDiagnoseFailure() {
// given
class FailingHook : PreStartHook {
override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PreStartHooks, reports: Reports) {
throw Exception("Failing deliberately before Jira starts")
}
}

val nodePlans = listOf(1, 2).map {
JiraNodePlan.Builder()
.installation(
Expand All @@ -81,13 +93,11 @@ class JiraDataCenterPlanIT {
)
)
.start(JiraLaunchScript())
.hooks(PreInstallHooks.default().also { Datasets.JiraSevenDataset.hookMysql(it.postStart) })
.hooks(PreInstallHooks.default().also { it.preStart.insert(FailingHook()) })
.build()
}
val instanceHooks = PreInstanceHooks.default()
// TODO this plus `EmptyJiraHome()` = failing `DatabaseIpConfig` - couple them together or stop expecting a preexisting `dbconfig.xml`? but then what about missing lucene indexes?
.also { Datasets.JiraSevenDataset.hookMysql(it, infrastructure) }
val dcPlan = JiraDataCenterPlan(nodePlans, instanceHooks, Supplier { TODO() }, infrastructure)
val balancerPlan = ApacheProxyPlan(80, infrastructure)
val dcPlan = JiraDataCenterPlan(nodePlans, PreInstanceHooks.default(), balancerPlan, infrastructure)

try {
// when
Expand Down

0 comments on commit 59b144e

Please sign in to comment.