-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
JPERF-273 Switch to hooks preserving backwards compatibility
Consume infra 3aacf87 Next steps: * pristine Jira DC Plan unencumbered by `LegacyAwsInfrastructure` or CFN * bridge from `JiraInstancePlan` to `JiraFormula` if possible * bridge the other way around if possible * can `SambaSharedHome` really work with just `TcpNode`? * do we actually need `Reports` transport via S3? * do we need bulk/directory transport for `Reports`? * add jpt-workspace as CI artifact * make hook collections append-only (`PreInstanceHooks` already are)
- Loading branch information
1 parent
096dc05
commit 7768137
Showing
9 changed files
with
618 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
...kotlin/com/atlassian/performance/tools/awsinfrastructure/api/database/AsyncInstallHook.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.atlassian.performance.tools.awsinfrastructure.api.database | ||
|
||
import com.atlassian.performance.tools.concurrency.api.submitWithLogContext | ||
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.hook.PostInstallHook | ||
import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PostInstallHooks | ||
import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHook | ||
import com.atlassian.performance.tools.infrastructure.api.jira.install.hook.PreInstallHooks | ||
import com.atlassian.performance.tools.infrastructure.api.jira.report.Reports | ||
import com.atlassian.performance.tools.ssh.api.SshConnection | ||
import java.util.concurrent.Executors | ||
import java.util.concurrent.Future | ||
|
||
/** | ||
* Begins to run [hook] during [PreInstallHooks] and finishes during [PostInstallHooks] | ||
*/ | ||
class AsyncInstallHook( | ||
private val hook: PreInstallHook | ||
) : PreInstallHook { | ||
|
||
override fun call(ssh: SshConnection, http: HttpNode, hooks: PreInstallHooks, reports: Reports) { | ||
val thread = Executors.newSingleThreadExecutor() | ||
val future = thread.submitWithLogContext("async-hook") { | ||
hook.call(ssh, http, hooks, reports) | ||
} | ||
hooks.postInstall.insert(FutureHook(future)) | ||
} | ||
} | ||
|
||
private class FutureHook( | ||
private val future: Future<*> | ||
) : PostInstallHook { | ||
|
||
override fun call(ssh: SshConnection, jira: InstalledJira, hooks: PostInstallHooks, reports: Reports) { | ||
future.get() | ||
} | ||
} |
261 changes: 261 additions & 0 deletions
261
...lin/com/atlassian/performance/tools/awsinfrastructure/api/jira/LegacyAwsInfrastructure.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
package com.atlassian.performance.tools.awsinfrastructure.api.jira | ||
|
||
import com.amazonaws.services.cloudformation.model.Parameter | ||
import com.amazonaws.services.ec2.model.Filter | ||
import com.amazonaws.services.ec2.model.Tag | ||
import com.atlassian.performance.tools.aws.api.* | ||
import com.atlassian.performance.tools.awsinfrastructure.TemplateBuilder | ||
import com.atlassian.performance.tools.awsinfrastructure.api.hardware.C4EightExtraLargeElastic | ||
import com.atlassian.performance.tools.awsinfrastructure.api.hardware.Computer | ||
import com.atlassian.performance.tools.awsinfrastructure.api.hardware.M4ExtraLargeElastic | ||
import com.atlassian.performance.tools.awsinfrastructure.api.hardware.Volume | ||
import com.atlassian.performance.tools.awsinfrastructure.api.loadbalancer.LoadBalancerFormula | ||
import com.atlassian.performance.tools.awsinfrastructure.api.network.Network | ||
import com.atlassian.performance.tools.awsinfrastructure.api.network.NetworkFormula | ||
import com.atlassian.performance.tools.awsinfrastructure.api.network.ProvisionedNetwork | ||
import com.atlassian.performance.tools.awsinfrastructure.aws.TokenScrollingEc2 | ||
import com.atlassian.performance.tools.infrastructure.api.jira.JiraNodeConfig | ||
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.start.hook.PreStartHooks | ||
import com.atlassian.performance.tools.infrastructure.api.loadbalancer.LoadBalancer | ||
import com.atlassian.performance.tools.infrastructure.api.loadbalancer.LoadBalancerPlan | ||
import com.atlassian.performance.tools.infrastructure.api.network.HttpServerRoom | ||
import com.atlassian.performance.tools.infrastructure.api.network.Networked | ||
import com.atlassian.performance.tools.infrastructure.api.network.TcpServerRoom | ||
import com.atlassian.performance.tools.ssh.api.Ssh | ||
import com.atlassian.performance.tools.ssh.api.SshHost | ||
import com.atlassian.performance.tools.workspace.api.RootWorkspace | ||
import org.apache.logging.log4j.LogManager | ||
import org.apache.logging.log4j.Logger | ||
import java.nio.file.Path | ||
import java.time.Duration | ||
import java.util.* | ||
import java.util.concurrent.CompletableFuture | ||
import java.util.function.Supplier | ||
|
||
class LegacyAwsInfrastructure private constructor( | ||
private val aws: Aws, | ||
private val investment: Investment, | ||
private val networking: Supplier<ProvisionedNetwork>, | ||
private val workspace: Path, | ||
private val jiraComputer: Computer, | ||
private val jiraVolume: Volume, | ||
private val jiraNodeConfigs: List<JiraNodeConfig>, | ||
private val databaseComputer: Computer, | ||
private val databaseVolume: Volume, | ||
private val provisioningTimout: Duration | ||
) : Networked, AutoCloseable { | ||
private val logger: Logger = LogManager.getLogger(this::class.java) | ||
private val nonce = UUID.randomUUID().toString() | ||
private val sshKey: SshKey by lazy { provisionKey() } | ||
private val provisionedNetwork: ProvisionedNetwork by lazy { networking.get() } | ||
private val network: Network by lazy { provisionedNetwork.network } | ||
private val provisioning: ProvisionedStack by lazy { provisionStack() } | ||
private val deprovisioning: CompletableFuture<*> by lazy { | ||
provisioning.release() | ||
provisionedNetwork.resource.release() | ||
sshKey.remote.release() | ||
} | ||
|
||
private fun provisionKey(): SshKey { | ||
return SshKeyFormula(aws.ec2, workspace, nonce, investment.lifespan).provision() | ||
} | ||
|
||
private fun provisionStack(): ProvisionedStack { | ||
logger.info("Setting up Jira stack...") | ||
val template = TemplateBuilder("2-nodes-dc.yaml").adaptTo(jiraNodeConfigs) | ||
val stack = StackFormula( | ||
investment = investment, | ||
cloudformationTemplate = template, | ||
parameters = listOf( | ||
Parameter() | ||
.withParameterKey("KeyName") | ||
.withParameterValue(sshKey.remote.name), | ||
Parameter() | ||
.withParameterKey("InstanceProfile") | ||
.withParameterValue(aws.shortTermStorageAccess()), | ||
Parameter() | ||
.withParameterKey("Ami") | ||
.withParameterValue(aws.defaultAmi), | ||
Parameter() | ||
.withParameterKey("JiraInstanceType") | ||
.withParameterValue(jiraComputer.instanceType.toString()), | ||
Parameter() | ||
.withParameterKey("JiraVolumeSize") | ||
.withParameterValue(jiraVolume.size.toString()), | ||
Parameter() | ||
.withParameterKey("DatabaseInstanceType") | ||
.withParameterValue(databaseComputer.instanceType.toString()), | ||
Parameter() | ||
.withParameterKey("DatabaseVolumeSize") | ||
.withParameterValue(databaseVolume.size.toString()), | ||
Parameter() | ||
.withParameterKey("Vpc") | ||
.withParameterValue(network.vpc.vpcId), | ||
Parameter() | ||
.withParameterKey("Subnet") | ||
.withParameterValue(network.subnet.subnetId) | ||
), | ||
aws = aws, | ||
pollingTimeout = provisioningTimout | ||
).provision() | ||
logger.info("Jira stack is provisioned, it will expire at ${stack.expiry}") | ||
return stack | ||
} | ||
|
||
override fun close() { | ||
deprovisioning.get() | ||
} | ||
|
||
override fun subnet(): String = network.subnet.cidrBlock | ||
|
||
val jiraNodesServerRoom: HttpServerRoom = StackJiraNodes() | ||
val databaseServerRoom: TcpServerRoom = StackDatabase() | ||
val sharedHomeServerRoom: TcpServerRoom = StackSharedHome() | ||
|
||
private fun listMachines() = provisioning.listMachines() | ||
|
||
fun balance(formula: LoadBalancerFormula): LoadBalancerPlan { | ||
return object : LoadBalancerPlan { | ||
override fun materialize(nodes: List<HttpNode>, hooks: List<PreStartHooks>): LoadBalancer { | ||
val filter = Filter( | ||
"network-interface.addresses.private-ip-address", | ||
nodes.map { it.tcp.privateIp } | ||
) | ||
val instances = TokenScrollingEc2(aws.ec2).findInstances(filter) | ||
return formula | ||
.provision( | ||
investment, | ||
instances, | ||
network.vpc, | ||
network.subnet, | ||
sshKey, | ||
aws | ||
) | ||
.loadBalancer | ||
} | ||
} | ||
} | ||
|
||
private inner class StackSharedHome : TcpServerRoom { | ||
|
||
override fun serveTcp(name: String): TcpNode { | ||
val machine = listMachines().single { it.tags.contains(Tag("jpt-shared-home", "true")) } | ||
val publicIp = machine.publicIpAddress | ||
val ssh = Ssh(SshHost(publicIp, "ubuntu", sshKey.file.path), connectivityPatience = 4) | ||
sshKey.file.facilitateSsh(publicIp) | ||
ssh.newConnection().use { jiraComputer.setUp(it) } | ||
return TcpNode( | ||
publicIp, | ||
machine.privateIpAddress, | ||
3306, | ||
name, | ||
ssh | ||
) | ||
} | ||
|
||
override fun serveTcp(name: String, tcpPorts: List<Int>, udpPorts: List<Int>): TcpNode { | ||
val ports = "TCP $tcpPorts and UDP $udpPorts" | ||
throw Exception( | ||
"It's unclear whether $ports are expected to be open to the public or privately." + | ||
" All ports are open within the VPC." | ||
) | ||
} | ||
} | ||
|
||
private inner class StackDatabase : TcpServerRoom { | ||
|
||
override fun serveTcp(name: String, tcpPorts: List<Int>, udpPorts: List<Int>): TcpNode { | ||
if (tcpPorts.singleOrNull() == 3306 && udpPorts.isEmpty()) { | ||
return serveTcp(name) | ||
} else { | ||
throw Exception("The stack is not prepared for TCP $tcpPorts and UDP $udpPorts") | ||
} | ||
} | ||
|
||
override fun serveTcp(name: String): TcpNode { | ||
val machine = listMachines().single { it.tags.contains(Tag("jpt-database", "true")) } | ||
val publicIp = machine.publicIpAddress | ||
val ssh = Ssh(SshHost(publicIp, "ubuntu", sshKey.file.path), connectivityPatience = 5) | ||
sshKey.file.facilitateSsh(publicIp) | ||
ssh.newConnection().use { databaseComputer.setUp(it) } | ||
return TcpNode( | ||
publicIp, | ||
machine.privateIpAddress, | ||
3306, | ||
name, | ||
ssh | ||
) | ||
} | ||
} | ||
|
||
private inner class StackJiraNodes : HttpServerRoom { | ||
|
||
private val machines by lazy { | ||
listMachines().filter { it.tags.contains(Tag("jpt-jira", "true")) } | ||
} | ||
private var nodesRequested = 0 | ||
|
||
override fun serveHttp(name: String): HttpNode { | ||
val machine = | ||
machines[nodesRequested++] // TODO looks like a yikes, relies on sync across `List<JiraNodeConfig>` and `List<JiraNodePlan>` | ||
val publicIp = machine.publicIpAddress | ||
val ssh = Ssh(SshHost(publicIp, "ubuntu", sshKey.file.path), connectivityPatience = 5) | ||
sshKey.file.facilitateSsh(publicIp) | ||
ssh.newConnection().use { jiraComputer.setUp(it) } | ||
val tcp = TcpNode( | ||
publicIp, | ||
machine.privateIpAddress, | ||
8080, | ||
name, | ||
ssh | ||
) | ||
return HttpNode(tcp, "/", false) | ||
} | ||
} | ||
|
||
class Builder( | ||
private var aws: Aws, | ||
private var investment: Investment | ||
) { | ||
private var networking: Supplier<ProvisionedNetwork> = | ||
Supplier { NetworkFormula(investment, aws).provisionAsResource() } | ||
private var jiraNodeConfigs: List<JiraNodeConfig> = listOf(1, 2).map { JiraNodeConfig.Builder().build() } | ||
private var jiraComputer: Computer = C4EightExtraLargeElastic() | ||
private var jiraVolume: Volume = Volume(100) | ||
private var databaseComputer: Computer = M4ExtraLargeElastic() | ||
private var databaseVolume: Volume = Volume(100) | ||
private var provisioningTimout: Duration = Duration.ofMinutes(30) | ||
private var workspace: Path = RootWorkspace().currentTask.isolateTest(investment.reuseKey()).directory | ||
|
||
fun aws(aws: Aws) = apply { this.aws = aws } | ||
fun networking(networking: Supplier<ProvisionedNetwork>) = apply { this.networking = networking } | ||
fun investment(investment: Investment) = apply { this.investment = investment } | ||
fun jiraNodeConfigs(jiraNodeConfigs: List<JiraNodeConfig>) = | ||
apply { this.jiraNodeConfigs = jiraNodeConfigs } | ||
|
||
fun jiraComputer(jiraComputer: Computer) = apply { this.jiraComputer = jiraComputer } | ||
fun jiraVolume(jiraVolume: Volume) = apply { this.jiraVolume = jiraVolume } | ||
fun databaseComputer(databaseComputer: Computer) = apply { this.databaseComputer = databaseComputer } | ||
fun databaseVolume(databaseVolume: Volume) = apply { this.databaseVolume = databaseVolume } | ||
fun provisioningTimout(provisioningTimout: Duration) = | ||
apply { this.provisioningTimout = provisioningTimout } | ||
|
||
fun workspace(workspace: Path) = apply { this.workspace = workspace } | ||
|
||
fun build(): LegacyAwsInfrastructure = LegacyAwsInfrastructure( | ||
aws = aws, | ||
networking = networking, | ||
investment = investment, | ||
jiraNodeConfigs = jiraNodeConfigs, | ||
jiraComputer = jiraComputer, | ||
jiraVolume = jiraVolume, | ||
databaseComputer = databaseComputer, | ||
databaseVolume = databaseVolume, | ||
provisioningTimout = provisioningTimout, | ||
workspace = workspace | ||
) | ||
} | ||
} | ||
|
49 changes: 49 additions & 0 deletions
49
src/main/kotlin/com/atlassian/performance/tools/awsinfrastructure/aws/TokenScrollingEc2.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package com.atlassian.performance.tools.awsinfrastructure.aws | ||
|
||
import com.amazonaws.services.ec2.AmazonEC2 | ||
import com.amazonaws.services.ec2.model.DescribeInstancesRequest | ||
import com.amazonaws.services.ec2.model.Filter | ||
import com.amazonaws.services.ec2.model.Instance | ||
import com.amazonaws.services.ec2.model.InstanceStateName.* | ||
import com.atlassian.performance.tools.aws.api.ScrollingEc2 | ||
|
||
/** | ||
* Scrolls through batches of AWS EC2 instances using "page token". | ||
*/ | ||
internal class TokenScrollingEc2( | ||
private val ec2: AmazonEC2 | ||
) : ScrollingEc2 { | ||
override fun scrollThroughInstances( | ||
vararg filters: Filter, | ||
batchAction: (List<Instance>) -> Unit | ||
) { | ||
var token: String? = null | ||
do { | ||
val response = ec2.describeInstances( | ||
DescribeInstancesRequest() | ||
.withFilters(filters.toList()) | ||
.withNextToken(token) | ||
) | ||
val batch = response | ||
.reservations | ||
.flatMap { it.instances } | ||
batchAction(batch) | ||
token = response.nextToken | ||
} while (token != null) | ||
} | ||
|
||
override fun findInstances( | ||
vararg filters: Filter | ||
): List<Instance> { | ||
val instances = mutableListOf<Instance>() | ||
scrollThroughInstances(*filters) { batch -> | ||
instances += batch | ||
} | ||
return instances | ||
} | ||
|
||
override fun allocated(): Filter = Filter( | ||
"instance-state-name", | ||
listOf(Pending, Running, ShuttingDown).map { it.toString() } | ||
) | ||
} |
Oops, something went wrong.