Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package ws.osiris.awsdeploy.cloudformation
import com.amazonaws.services.apigateway.AmazonApiGateway
import com.amazonaws.services.apigateway.model.GetRestApisRequest
import com.amazonaws.services.apigateway.model.RestApi
import com.amazonaws.services.cloudformation.AmazonCloudFormation
import com.amazonaws.services.cloudformation.model.CreateStackRequest
import com.amazonaws.services.cloudformation.model.DeleteStackRequest
import com.amazonaws.services.cloudformation.model.DescribeStackResourceRequest
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest
import com.amazonaws.services.cloudformation.model.ListStacksRequest
import com.amazonaws.services.cloudformation.model.StackStatus
import com.amazonaws.services.cloudformation.model.StackSummary
import com.amazonaws.services.cloudformation.model.UpdateStackRequest
import org.intellij.lang.annotations.Language
import org.slf4j.LoggerFactory
import ws.osiris.awsdeploy.AwsProfile
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
private val log = LoggerFactory.getLogger("ws.osiris.aws.cloudformation")
private const val CAPABILITY_NAMED_IAM = "CAPABILITY_NAMED_IAM"
/** If a stack has any of these statuses it must be updated. */
private val updateStatuses = setOf(
StackStatus.CREATE_COMPLETE,
StackStatus.UPDATE_COMPLETE,
StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS,
StackStatus.UPDATE_ROLLBACK_COMPLETE,
StackStatus.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS
)
/** If a stack has any of these statuses it must be created. */
private val createStatuses = setOf(
StackStatus.CREATE_FAILED,
StackStatus.DELETE_COMPLETE
)
/** If a stack has a status in this set it must be deleted and recreated. */
private val deleteStatuses = setOf(
StackStatus.ROLLBACK_COMPLETE
)
/** The statuses indicating a successful deployment. */
private val deployedStatuses = setOf(
StackStatus.CREATE_COMPLETE,
StackStatus.UPDATE_COMPLETE
)
/** If a stack has any of these statuses it indicates the operation succeeded. */
private val successStatuses = setOf(
StackStatus.CREATE_COMPLETE,
StackStatus.DELETE_COMPLETE,
StackStatus.UPDATE_COMPLETE
)
/** If a stack has any of these statuses it means the operation failed. */
private val failedStatuses = setOf(
StackStatus.CREATE_FAILED,
StackStatus.ROLLBACK_IN_PROGRESS,
StackStatus.ROLLBACK_FAILED,
StackStatus.DELETE_FAILED,
StackStatus.ROLLBACK_COMPLETE,
StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
StackStatus.UPDATE_ROLLBACK_FAILED,
StackStatus.UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS,
StackStatus.UPDATE_ROLLBACK_COMPLETE
)
// TODO parameter class for code hash / bucket / key?
/**
* Information about an API that has been deployed.
*/
class DeployResult(
/** true if a new stack was created, false if an existing stack was updated. */
val stackCreated: Boolean,
/** The ID of the API that was updated or created; the URL is https://$apiId.execute-api.$region.amazonaws.com/ */
val apiId: String,
/** The ARN of the lambda function version. */
val lambdaVersionArn: String,
/** The ARN of the lambda function that sends keep-alive messages; null if keep-alive is disabled. */
val keepAliveLambdaArn: String?
)
/**
* Deploys the CloudformationStack and returns the ID of the API Gateway API.
*/
fun deployStack(profile: AwsProfile, stackName: String, apiName: String, templateUrl: String): DeployResult {
log.debug("Deploying stack to region {} using template {}", profile.region, templateUrl)
val stackSummaries = profile.cloudFormationClient.listAllStacks()
val liveStacks = stackSummaries.filter { it.stackName == stackName && it.stackStatus != "DELETE_COMPLETE" }
val (_, created) = if (liveStacks.isEmpty()) {
val stackId = createStack(stackName, profile.cloudFormationClient, templateUrl); Pair(stackId, true)
} else if (liveStacks.size > 1) {
throw IllegalStateException("Found multiple stacks named '$stackName': $liveStacks")
} else {
val stackSummary = liveStacks[0]
val status = StackStatus.fromValue(stackSummary.stackStatus)
if (deleteStatuses.contains(status)) {
deleteStack(stackName, profile.cloudFormationClient)
val stackId = createStack(stackName, profile.cloudFormationClient, templateUrl)
Pair(stackId, true)
} else if (updateStatuses.contains(status)) {
val stackId = updateStack(stackName, templateUrl, profile)
Pair(stackId, false)
} else if (createStatuses.contains(status)) {
val stackId = createStack(stackName, profile.cloudFormationClient, templateUrl)
Pair(stackId, true)
} else {
throw IllegalStateException("Unable to deploy stack '$stackName' with status ${stackSummary.stackStatus}")
}
}
val apiStackResourceResult = profile.cloudFormationClient.describeStackResource(DescribeStackResourceRequest().apply {
this.stackName = stackName
this.logicalResourceId = "ApiStack"
})
val describeResult = profile.cloudFormationClient.describeStacks(DescribeStacksRequest().apply {
this.stackName = apiStackResourceResult.stackResourceDetail.physicalResourceId
})
if (describeResult.stacks.size != 1) throw IllegalStateException("Multiple stacks found: ${describeResult.stacks}")
val stack = describeResult.stacks[0]
val keepAliveLambdaArn = stack.outputs.find { it.outputKey == "KeepAliveLambdaArn" }?.outputValue
val lambdaVersionArn = stack.outputs.find { it.outputKey == "LambdaVersionArn" }?.outputValue!!
val status = StackStatus.fromValue(stack.stackStatus)
if (!deployedStatuses.contains(status)) throw IllegalStateException("Stack status is ${stack.stackStatus}")
return DeployResult(created, apiId(apiName, profile), lambdaVersionArn, keepAliveLambdaArn)
}
/**
* Extension function for the CloudFormation client that lists all stacks and deals with paging in the AWS SDK.
*/
fun AmazonCloudFormation.listAllStacks(): List {
fun listAllStacks(token: String?): List {
val result = listStacks(ListStacksRequest().apply { nextToken = token })
return if (result.nextToken == null) {
result.stackSummaries
} else {
result.stackSummaries + listAllStacks(result.nextToken)
}
}
return listAllStacks(null)
}
//--------------------------------------------------------------------------------------------------
internal fun apiId(apiName: String, profile: AwsProfile): String {
return profile.apiGatewayClient.getAllRestApis().find { it.name == apiName }?.id
?: throw IllegalStateException("No API found with name '$apiName'")
}
fun AmazonApiGateway.getAllRestApis(): List {
fun restApis(token: String?): List {
val result = getRestApis(GetRestApisRequest().apply { position = token })
val apis = result.items
return if (result.position == null) {
apis
} else {
apis + restApis(result.position)
}
}
return restApis(null)
}
/**
* Returns the name of the API Gateway API for the given application and environment names.
*/
internal fun apiName(appName: String, envName: String?): String = if (envName == null) appName else "$appName.$envName"
private fun deleteStack(apiName: String, cloudFormationClient: AmazonCloudFormation) {
log.info("Deleting stack '$apiName'")
val describeResult = cloudFormationClient.describeStacks(DescribeStacksRequest().apply { stackName = apiName })
val stackId = describeResult.stacks[0].stackId
cloudFormationClient.deleteStack(DeleteStackRequest().apply { stackName = apiName })
waitForStack(stackId, cloudFormationClient)
log.info("Deleted stack '$apiName'")
}
private fun updateStack(
appName: String,
templateUrl: String,
profile: AwsProfile
): String? {
log.info("Updating stack '{}'", appName)
val updateResult = profile.cloudFormationClient.updateStack(UpdateStackRequest().apply {
stackName = appName
templateURL = templateUrl
setCapabilities(listOf(CAPABILITY_NAMED_IAM))
})
waitForStack(updateResult.stackId, profile.cloudFormationClient)
log.info("Stack updated. ID = ${updateResult.stackId}")
return updateResult.stackId
}
private fun createStack(apiName: String, cloudFormationClient: AmazonCloudFormation, templateUrl: String): String? {
log.info("Creating stack '{}'", apiName)
val createResult = cloudFormationClient.createStack(CreateStackRequest().apply {
stackName = apiName
templateURL = templateUrl
setCapabilities(listOf(CAPABILITY_NAMED_IAM))
})
waitForStack(createResult.stackId, cloudFormationClient)
log.info("Stack created. ID = ${createResult.stackId}")
return createResult.stackId
}
// TODO can this be done using waiters?
/**
* Waits for a stack to finish deploying; returns when the stack status indicates success (`CREATE_COMPLETE` or
* `UPDATE_COMPLETE` or `DELETE_COMPLETE`). Throws an exception if the state indicates that deployment
* has failed.
*
* The looping and polling is necessary because the AWS APIs don't provide any efficient way to find out
* when an operation is actually complete. The so-called synchronous API returns as soon as the operation
* has started. This means that the stack is normally still being created or updated when the call returns
* so there's no way to know whether deployment was successful.
*/
private fun waitForStack(stackId: String, cloudFormationClient: AmazonCloudFormation) {
tailrec fun waitForStack(count: Int) {
val stackSummary = cloudFormationClient.listAllStacks().filter { it.stackId == stackId }[0]
val status = StackStatus.fromValue(stackSummary.stackStatus)
if (successStatuses.contains(status)) {
log.debug("Stack status $status, returning")
return
} else if (failedStatuses.contains(status)) {
throw IllegalStateException("Deployment failed, stack status: $status")
} else {
log.debug("Stack status $status, waiting")
if (count % 5 == 0) log.info("Waiting for stack to deploy...")
Thread.sleep(1000)
return waitForStack(count + 1)
}
}
return waitForStack(1)
}
// --------------------------------------------------------------------------------------------------
/**
* Represents a single CloudFormation YAML file.
*/
internal interface CloudFormationFile {
fun write(templatesDir: Path)
}
/**
* Represents the main generated CloudFormation template (`.template`).
*
* This contains all the main application resources. The only resources generated by Osiris that aren't
* in this file are REST resources that won't fit within the 200-resources-per-file limit.
*
* If there are REST resources defined in another file then they are referenced from this file
* as nested stacks.
*/
internal class MainCloudFormationFile internal constructor(
private val fileName: String,
private val parametersTemplate: ParametersTemplate,
private val outputsTemplate: OutputsTemplate,
private val apiTemplate: ApiTemplate,
private val deploymentTemplate: DeploymentTemplate,
private val restStackTemplates: List,
vararg templates: Template?
) : CloudFormationFile {
private val templates: List = templates.toList()
override fun write(templatesDir: Path) {
val filePath = templatesDir.resolve(fileName)
Files.newBufferedWriter(filePath, StandardOpenOption.CREATE).use { writer ->
log.info("Writing CloudFormation template {}", filePath.toAbsolutePath())
parametersTemplate.write(writer)
writer.write("Resources:")
for (template in templates.filterNotNull()) {
log.debug("Writing template of type {}", template.javaClass.simpleName)
template.write(writer)
}
log.debug("Writing root resource template")
apiTemplate.write(writer)
deploymentTemplate.write(writer, restStackTemplates.map { it.name }, apiTemplate.rootResource)
for (template in restStackTemplates) {
template.write(writer)
}
outputsTemplate.write(writer)
}
}
}
/**
* Represents a CloudFormation template defining a nested stack containing nothing but API Gateway
* REST resources and methods.
*/
internal class RestCloudFormationFile(
private val fileName: String,
private val templates: List,
private val authorizerParam: Boolean
) : CloudFormationFile {
override fun write(templatesDir: Path) {
val filePath = templatesDir.resolve(fileName)
Files.newBufferedWriter(filePath, StandardOpenOption.CREATE).use { writer ->
log.info("Writing CloudFormation template {}", filePath.toAbsolutePath())
// TODO this should go in a template impl for consistency
@Language("yaml")
val header = """
Parameters:
Api:
Type: String
Description: ID of the API Gateway API
ParentResourceId:
Type: String
Description: ID of the API Gateway resource
LambdaArn:
Type: String
Description: ARN of the lambda function version
""".trimIndent()
@Language("yaml")
val authorizerTemplate = """
|
| Authorizer:
| Type: String
| Description: ID of the custom authorizer
""".trimMargin()
writer.write(header)
if (authorizerParam) {
writer.write(authorizerTemplate)
}
writer.write("\n")
writer.write("Resources:")
writer.write("\n")
for (template in templates) {
log.debug("Writing resource template with path part {}", template.pathPart)
// All resources in this file must be children of the same resource (whose ID is a parameter)
template.write(writer, "!Ref ParentResourceId", "LambdaArn")
}
}
}
}