Skip to content

Commit

Permalink
add support for so caled interpolation of values in a properties map
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas3oo committed Nov 1, 2022
1 parent e837c1d commit 88d8709
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .settings/org.eclipse.buildship.core.prefs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
connection.project.dir=../spring-aws-serverless
connection.project.dir=../s3deploy
eclipse.preferences.version=1
71 changes: 66 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Apply the plugin to your project.

```groovy
plugins {
id 'se.solrike.cloudformation' version '1.0.0-beta.1'
id 'se.solrike.cloudformation' version '1.0.0-beta.2'
}
```
Gradle 7.0 or later must be used.
Expand All @@ -23,7 +23,7 @@ The tasks have to be created. Minimal example on how to create a task that creat

```groovy
plugins {
id 'se.solrike.cloudformation' version '1.0.0-beta.1'
id 'se.solrike.cloudformation' version '1.0.0-beta.2'
}
task deployS3Stack(type: se.solrike.cloudformation.CreateOrUpdateStackTask) {
parameters = [ s3BucketName : 's3-bucket4711']
Expand Down Expand Up @@ -65,6 +65,10 @@ The task will also create the following tags on the stack:
* TemplateGitVersion - If the Gradle project is using GIT then the last commit info for the template is added.
* CreatedBy/UpdatedBy - The local OS user's userID that executes the task.

## How the DeleteStackTask works

## How the PrintEnviromentParametersTask works

## More advanced example
### Example 1
Some stacks need additional permission when they are created, called "capabilities".
Expand Down Expand Up @@ -128,9 +132,9 @@ If you have all sorts of properties in the properties file you can filter out th
```java
# Java properties file for stage 25
# Properties for the S3 template
slrf.deploy.s3.s3BucketName: my-bucket-for-stage25
slrk.deploy.s3.s3BucketName: my-bucket-for-stage25
# Some property for another template
slrf.deploy.sam.handler: se.solrike.cloud.serverless.StreamLambdaHandler::handleRequest
slrk.deploy.sam.handler: se.solrike.cloud.serverless.StreamLambdaHandler::handleRequest
```


Expand All @@ -139,12 +143,69 @@ task deployS3Stack(type: se.solrike.cloudformation.CreateOrUpdateStackTask) {
group = 'AWS'
description = 'Create S3 buckets using Cloudformation template. ' +
'Specify the environment to use with -Penv=<my-env>.'
parameterPrefix = 'slrk.deploy.s3.' // only include properties which begins with 'slrk.deploy.s3.'
parameters = project.objects.mapProperty(String, String).convention(project.provider({
Properties props = new Properties()
file("environments/${env}.properties").withInputStream { props.load(it) }
props
}))
parameterPrefix = 'slrf.deploy.s3.' // only include properties which begins with 'slrf.deploy.s3.'
stackName = project.objects.property(String).convention(project.provider( {"s3-buckets-${env}"} ))
templateFileName = 'aws-cloudformation/aws-s3-buckets.yaml'
}
```

### Example 3
In a large complex setup there are more than one template and consequently a lot more stack parameters. Sometime you need to have the same value as input to several stacks and following DRY you need to be able to have variables in the properties files where you have the stacks parameters.

The following example has two stacks which shares an S3 bucket. One creates it and the other consumes it. So the bucket name needs to be as a parameter to both of them. Also the a DB password is needed and you don't want to have that as clear text in the environment's properties files. The example is simply base 64 encoding the password but in real life it shall be encrypted of course.

Properties file:

```java
# Java properties file for stage 25.
# Values are "interpolated" (evaluated as they where Groovy script) so it means those can reference
# other parameter and also be functions.
# common props
slrk.deploy.env.name: ${env}
# Properties for the S3 template
slrk.deploy.s3.s3BucketName: my-bucket-for-${slrk.deploy.env.name}
# Some property for another template
slrk.deploy.sam.handler: se.solrike.cloud.serverless.StreamLambdaHandler::handleRequest
slrk.deploy.sam.s3BucketName: ${slrk.deploy.s3.s3BucketName}
# DB user and password
slrk.deploy.db.user: application-user
slrk.deploy.db.password: ${MyUtils.decode('bXlub3Rzb3NlY3JldHBhc3N3b3JkCg==')}
```

Task to deploy and code in build.gradle to support the task.

```groovy
Properties loadProperties(String env) {
Properties props = new Properties()
file("environments/${env}.properties").withInputStream { props.load(it) }
// so that the property 'env' can be referenced in the env's properties file
props.setProperty('env', env)
return props
}
class MyUtils {
static String decode(String text) {
return new String(Base64.getDecoder().decode(text)).trim()
}
}
task deployS3Stack(type: se.solrike.cloudformation.CreateOrUpdateStackTask) {
group = 'AWS'
description = 'Create S3 buckets using Cloudformation template. ' +
'Specify the enviroment to use with -Penv=<my-env>.'
parameterPrefix = 'slrk.deploy.s3.' // only include properties which begins with 'slrf.deploy.s3.'
parameters = project.objects.mapProperty(String, String).convention(project.provider({loadProperties(env)}))
// pass in classloader so the MyUtils class above can be referenced in the env's properties file
parentClassLoader = getClass().getClassLoader()
stackName = project.objects.property(String).convention(project.provider( {"s3-buckets-${env}"} ))
templateFileName = 'aws-cloudformation/aws-s3-buckets.yaml'
}
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies {
}

group = 'se.solrike.cloudformation'
version = '1.0.0-beta.1'
version = '1.0.0-beta.2'
sourceCompatibility = '11'
targetCompatibility = '11'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,21 @@ abstract class CreateOrUpdateStackTask extends DefaultTask {
public abstract Property<Boolean> getEnableTerminationProtection()

/**
* The template parameters.
* Optionally specify a parameter name prefix to filter out the parameters to use.
* <p>
* Parameter names will be converted to PascalCase-ish since that is what Cloudformation templates expects.
* The submitted parameter names will have the prefix string removed from the names.
*/
@Input
public abstract MapProperty<String, String> getParameters()
@Optional
public abstract Property<String> getParameterPrefix()

/**
* Optionally specify a parameter name prefix to filter out the parameters to use.
* The template parameters.
* <p>
* The submitted parameter names will have the prefix string removed from the names.
* Parameter names will be converted to PascalCase-ish since that is what Cloudformation templates expects.
*/
@Input
@Optional
public abstract Property<String> getParameterPrefix()
public abstract MapProperty<String, String> getParameters()

/**
* Stack name.
Expand Down
60 changes: 60 additions & 0 deletions src/main/groovy/se/solrike/cloudformation/ParameterResolver.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package se.solrike.cloudformation

/**
* @author Lucas Persson
*/
public class ParameterResolver {

public static Map<String, String> resolve(Map<String, String> parameters, ClassLoader parentClassLoader) {
Binding binding = new Binding()

Properties p = new Properties()
p.putAll(parameters)
// ConfigSlurper will create the properties as beans in case a dot notation is used
ConfigObject conf = new ConfigSlurper().parse(p)
conf.each { key, value ->
binding.setVariable(key, value)
}
GroovyShell shell = new GroovyShell(parentClassLoader, binding)

// multipass so nested references (in two hops) can be used.
resolveValues(shell, binding)
resolveValues(shell, binding)

Map flatMap = [:]
flatternMap(binding.getVariables(), flatMap, null)

return flatMap
}

static void resolveValues(GroovyShell shell, Binding binding) {
visitMap(binding.getVariables(), {
it.value = shell.evaluate("\"$it.value\"")
})
}

static void visitMap(Map map, Closure action) {
map.each {
if (it.value instanceof Map) {
visitMap(it.value, action)
}
else {
action.call(it)
}
}
}

// flattern the map and re-create the dot notation or the keys
static void flatternMap(Map source, Map target, String prefix) {
source.each {
String key = prefix ? prefix + '.' + it.key : it.key
if (it.value instanceof Map) {
flatternMap(it.value, target, key)
}
else {
target[key] = it.value
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package se.solrike.cloudformation

import org.gradle.api.DefaultTask
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

import software.amazon.awssdk.core.waiters.WaiterResponse
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.Capability
import software.amazon.awssdk.services.cloudformation.model.CloudFormationException
import software.amazon.awssdk.services.cloudformation.model.CreateStackRequest
import software.amazon.awssdk.services.cloudformation.model.CreateStackResponse
import software.amazon.awssdk.services.cloudformation.model.DeleteStackResponse
import software.amazon.awssdk.services.cloudformation.model.DescribeStacksResponse
import software.amazon.awssdk.services.cloudformation.model.Parameter
import software.amazon.awssdk.services.cloudformation.model.Tag
import software.amazon.awssdk.services.cloudformation.model.UpdateStackRequest
import software.amazon.awssdk.services.cloudformation.model.UpdateStackResponse

/**
* Task to print the resolved parameters for an environment.
* <p>
* The task will first resolve parameters using Groovy's evaluation support.
*
* @author Lucas Persson
*/
abstract class PrintEnviromentParametersTask extends DefaultTask {

/**
* The build script's class loader so that this task can find e.g. classes defined in the build.gradle.
*/
@Input
@Optional
public abstract Property<ClassLoader> getParentClassLoader()

/**
* The parameters.
*/
@Input
public abstract MapProperty<String, String> getParameters()



@TaskAction
void execute() {
Map resolvedParameters = ParameterResolver.resolve(getParameters().get(),
getParentClassLoader().getOrElse(getClass().getClassLoader()))

resolvedParameters.sort { it.key }.each {key, value ->
println "$key : $value"
}
}
}

0 comments on commit 88d8709

Please sign in to comment.