diff --git a/CHANGELOG.md b/CHANGELOG.md index ef338388..f3200c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * [Issue #303](https://github.com/manheim/terraform-pipeline/issues/303) Update ValidateFormatPlugin Documentation - additional options command * [Issue #300](https://github.com/manheim/terraform-pipeline/issues/300) Trim whitespace when detecting terraform version from file. +* [Issue #299](https://github.com/manheim/terraform-pipeline/issues/299) Support for Global AWS Parameter Store # v5.11 diff --git a/docs/ParameterStoreBuildWrapperPlugin.md b/docs/ParameterStoreBuildWrapperPlugin.md index 3d63abb4..c9a0a6cd 100644 --- a/docs/ParameterStoreBuildWrapperPlugin.md +++ b/docs/ParameterStoreBuildWrapperPlugin.md @@ -61,3 +61,34 @@ validate.then(deployQA) .then(deployProd) .build() ``` + + +You can also optionally support Global Parameters applied to the following stages `VALIDATE`, `PLAN`, `APPLY` + + +``` +// Jenkinsfile +@Library(['terraform-pipeline@v5.0']) _ + +Jenkinsfile.init(this) + +ParameterStoreBuildWrapperPlugin.withGlobalParameter('/somePath/') // get all keys under `/somePath/` + .withGlobalParameter('/someOtherPath/', [naming: 'relative', recursive: true]) // get all keys recursively under `/someOtherPath/` + .init() // Enable ParameterStoreBuildWrapperPlugin + +def validate = new TerraformValidateStage() + +// Inject all parameters in ///qa +def deployQA = new TerraformEnvironmentStage('qa') + +// Inject all parameters in ///uat +def deployUat = new TerraformEnvironmentStage('uat') + +// Inject all parameters in ///prod +def deployProd = new TerraformEnvironmentStage('prod') + +validate.then(deployQA) + .then(deployUat) + .then(deployProd) + .build() +``` \ No newline at end of file diff --git a/src/ParameterStoreBuildWrapperPlugin.groovy b/src/ParameterStoreBuildWrapperPlugin.groovy index 84123de2..8b5ad29d 100644 --- a/src/ParameterStoreBuildWrapperPlugin.groovy +++ b/src/ParameterStoreBuildWrapperPlugin.groovy @@ -1,12 +1,15 @@ +import static TerraformValidateStage.ALL import static TerraformEnvironmentStage.PLAN import static TerraformEnvironmentStage.APPLY -class ParameterStoreBuildWrapperPlugin implements TerraformEnvironmentStagePlugin { +class ParameterStoreBuildWrapperPlugin implements TerraformValidateStagePlugin, TerraformEnvironmentStagePlugin { private static globalPathPattern + private static List globalParameterOptions = [] private static defaultPathPattern = { options -> "/${options['organization']}/${options['repoName']}/${options['environment']}/" } public static void init() { TerraformEnvironmentStage.addPlugin(new ParameterStoreBuildWrapperPlugin()) + TerraformValidateStage.addPlugin(new ParameterStoreBuildWrapperPlugin()) } public static withPathPattern(Closure newPathPattern) { @@ -14,33 +17,61 @@ class ParameterStoreBuildWrapperPlugin implements TerraformEnvironmentStagePlugi return this } + public static withGlobalParameter(String path, Map options = [:]) { + globalParameterOptions << [path: path] + options + return this + } + + @Override + public void apply(TerraformValidateStage stage) { + globalParameterOptions.each { gp -> + stage.decorate(ALL, addParameterStoreBuildWrapper(gp)) + } + } + @Override public void apply(TerraformEnvironmentStage stage) { def environment = stage.getEnvironment() - def parameterStorePath = pathForEnvironment(environment) + List options = getParameterOptions(environment) + + options.each { option -> + stage.decorate(PLAN, addParameterStoreBuildWrapper(option)) + stage.decorate(APPLY, addParameterStoreBuildWrapper(option)) + } + } + + List getParameterOptions(String environment) { + List options = [] + options.add(getEnvironmentParameterOptions(environment)) + options.addAll(getGlobalParameterOptions()) + + return options + } + + List getGlobalParameterOptions() { + return globalParameterOptions + } - def options = [ - path: parameterStorePath, + Map getEnvironmentParameterOptions(String environment) { + return [ + path: pathForEnvironment(environment), credentialsId: "${environment.toUpperCase()}_PARAMETER_STORE_ACCESS" ] - - stage.decorate(PLAN, addParameterStoreBuildWrapper(options)) - stage.decorate(APPLY, addParameterStoreBuildWrapper(options)) } String pathForEnvironment(String environment) { String organization = Jenkinsfile.instance.getOrganization() - String repoName = Jenkinsfile.instance.getRepoName() - def patternOptions = [ environment: environment, - repoName: repoName, - organization: organization ] + String repoName = Jenkinsfile.instance.getRepoName() + def patternOptions = [ environment: environment, + repoName: repoName, + organization: organization ] def pathPattern = globalPathPattern ?: defaultPathPattern return pathPattern(patternOptions) } - public static Closure addParameterStoreBuildWrapper(Map options = []) { + public Closure addParameterStoreBuildWrapper(Map options = [:]) { def Map defaultOptions = [ naming: 'basename' ] @@ -56,5 +87,6 @@ class ParameterStoreBuildWrapperPlugin implements TerraformEnvironmentStagePlugi public static reset() { globalPathPattern = null + globalParameterOptions = [] } } diff --git a/test/ParameterStoreBuildWrapperPluginTest.groovy b/test/ParameterStoreBuildWrapperPluginTest.groovy index dbe9a1df..82830207 100644 --- a/test/ParameterStoreBuildWrapperPluginTest.groovy +++ b/test/ParameterStoreBuildWrapperPluginTest.groovy @@ -4,6 +4,13 @@ import static org.junit.Assert.assertEquals import static org.junit.Assert.assertThat import static org.mockito.Mockito.mock import static org.mockito.Mockito.when +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.eq +import static org.mockito.Mockito.doReturn +import static org.mockito.Mockito.never +import static org.mockito.Mockito.times +import static org.mockito.Mockito.anyString import org.junit.After import org.junit.Test @@ -15,6 +22,7 @@ class ParameterStoreBuildWrapperPluginTest { public class Init { @After void resetPlugins() { + TerraformValidateStage.resetPlugins() TerraformEnvironmentStage.reset() } @@ -25,6 +33,204 @@ class ParameterStoreBuildWrapperPluginTest { Collection actualPlugins = TerraformEnvironmentStage.getPlugins() assertThat(actualPlugins, hasItem(instanceOf(ParameterStoreBuildWrapperPlugin.class))) } + + @Test + void modifiesTerraformValidateStageCommand() { + ParameterStoreBuildWrapperPlugin.init() + + Collection actualPlugins = TerraformValidateStage.getPlugins() + assertThat(actualPlugins, hasItem(instanceOf(ParameterStoreBuildWrapperPlugin.class))) + } + } + + public class Apply { + @After + public void reset() { + ParameterStoreBuildWrapperPlugin.reset() + } + + class WithTerraformValidateStage { + @Test + void doesNotDecorateTheTerraformValidateStageIfGlobalParametersNotSet() { + def expectedClosure = { -> } + TerraformValidateStage stage = mock(TerraformValidateStage.class) + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + plugin.apply(stage) + + verify(stage, never()).decorate(expectedClosure) + } + + @Test + void decorateTheTerraformValidateStageIfGlobalParametersSet() { + String path = '/somePath/' + def expectedClosure = { -> } + Map gp = [path: path] + TerraformValidateStage stage = mock(TerraformValidateStage.class) + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(expectedClosure).when(plugin).addParameterStoreBuildWrapper(gp) + + plugin.withGlobalParameter(path) + plugin.apply(stage) + + verify(stage).decorate(TerraformValidateStage.ALL, expectedClosure) + } + } + + class WithTerraformEnvironmentStage { + @Test + void doesNotDecorateTheTerraformEnvironmentStageIfNoOptionsSet() { + def expectedClosure = { -> } + String environment = "MyEnv" + List options = [] + TerraformEnvironmentStage stage = mock(TerraformEnvironmentStage.class) + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(environment).when(stage).getEnvironment() + doReturn(options).when(plugin).getParameterOptions(environment) + + plugin.apply(stage) + + verify(stage, never()).decorate(expectedClosure) + } + + @Test + void decorateTheTerraformEnvironmentStageWhenSingleOptionsSet() { + def expectedClosure = { -> } + String environment = "MyEnv" + List options = [[someKey: "someValue"]] + TerraformEnvironmentStage stage = mock(TerraformEnvironmentStage.class) + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(environment).when(stage).getEnvironment() + doReturn(options).when(plugin).getParameterOptions(environment) + doReturn(expectedClosure).when(plugin).addParameterStoreBuildWrapper(options[0]) + + plugin.apply(stage) + + verify(stage).decorate(TerraformEnvironmentStage.PLAN, expectedClosure) + verify(stage).decorate(TerraformEnvironmentStage.APPLY, expectedClosure) + } + + @Test + void decorateTheTerraformEnvironmentStageWhenMultipleOptionsSet() { + def firstClosure = { -> } + def secondClosure = { -> } + String environment = "MyEnv" + List options = [[someKey: "someValue"], [someOtherKey: "someOtherValue"]] + TerraformEnvironmentStage stage = mock(TerraformEnvironmentStage.class) + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(environment).when(stage).getEnvironment() + doReturn(options).when(plugin).getParameterOptions(environment) + doReturn(firstClosure).when(plugin).addParameterStoreBuildWrapper(options[0]) + doReturn(secondClosure).when(plugin).addParameterStoreBuildWrapper(options[1]) + + plugin.apply(stage) + + verify(stage, times(2)).decorate(anyString(), eq(firstClosure)) + verify(stage, times(2)).decorate(anyString(), eq(secondClosure)) + } + } + } + + class GetParameterOptions { + @After + public void reset() { + ParameterStoreBuildWrapperPlugin.reset() + } + + @Test + void returnsEnvironmentOptionWhenSet() { + String environment = "MyEnv" + Map option = [ key: "value" ] + List expected = [ option ] + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(option).when(plugin).getEnvironmentParameterOptions(environment) + + List actual = plugin.getParameterOptions(environment) + + assertEquals(expected, actual) + } + + @Test + void returnsSingleGlobalOptionsAndEnvironmentOptionWhenSet() { + String environment = "MyEnv" + Map environmentOption = [ env: "envValue" ] + Map globalOption1 = [ global: "globalValue" ] + List globalOptions = [ globalOption1 ] + List expected = [ environmentOption ] + globalOptions + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(environmentOption).when(plugin).getEnvironmentParameterOptions(environment) + doReturn(globalOptions).when(plugin).getGlobalParameterOptions() + + List actual = plugin.getParameterOptions(environment) + + assertEquals(expected, actual) + } + + @Test + void returnsMultipleGlobalOptionsAndEnvironmentOptionWhenSet() { + String environment = "MyEnv" + Map environmentOption = [ env: "envValue" ] + Map globalOption1 = [ global: "globalValue" ] + Map globalOption2 = [ global2: "globalValue2" ] + List globalOptions = [ globalOption1, globalOption2 ] + List expected = [ environmentOption ] + globalOptions + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + + doReturn(environmentOption).when(plugin).getEnvironmentParameterOptions(environment) + doReturn(globalOptions).when(plugin).getGlobalParameterOptions() + + List actual = plugin.getParameterOptions(environment) + + assertEquals(expected, actual) + } + } + + public class GetEnvironmentParameterOptions { + @After + public void reset() { + Jenkinsfile.instance = null + ParameterStoreBuildWrapperPlugin.reset() + } + + private configureJenkins(Map config = [:]) { + Jenkinsfile.instance = mock(Jenkinsfile.class) + when(Jenkinsfile.instance.getStandardizedRepoSlug()).thenReturn(config.repoSlug) + when(Jenkinsfile.instance.getRepoName()).thenReturn(config.repoName ?: 'repo') + when(Jenkinsfile.instance.getOrganization()).thenReturn(config.organization ?: 'org') + when(Jenkinsfile.instance.getEnv()).thenReturn(config.env ?: [:]) + } + + @Test + void returnsTheCorrectParameterPathBasedOnEnvironment() { + String environment = "qa" + String expectedPath = "somePath" + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + doReturn(expectedPath).when(plugin).pathForEnvironment(environment) + + Map actual = plugin.getEnvironmentParameterOptions(environment) + + assertEquals(expectedPath, actual.path) + } + + @Test + void returnsTheCorrectCredentialsIdBasedOnEnvironment() { + String environment = "qa" + String path = "somePath" + String expectedCredentialsId = "${environment.toUpperCase()}_PARAMETER_STORE_ACCESS" + + ParameterStoreBuildWrapperPlugin plugin = spy(new ParameterStoreBuildWrapperPlugin()) + doReturn(path).when(plugin).pathForEnvironment(environment) + + Map actual = plugin.getEnvironmentParameterOptions(environment) + + assertEquals(expectedCredentialsId, actual.credentialsId.toString()) + } } public class PathForEnvironment { @@ -84,5 +290,56 @@ class ParameterStoreBuildWrapperPluginTest { assertEquals(ParameterStoreBuildWrapperPlugin.class, result) } } + + class withGlobalParameter { + @After + public void reset() { + ParameterStoreBuildWrapperPlugin.reset() + } + + @Test + void addGlobalParameterWithNoOptions() { + String path = '/path/' + def result = ParameterStoreBuildWrapperPlugin.withGlobalParameter(path) + println(result) + assertEquals([[path: '/path/']], result.globalParameterOptions) + } + + @Test + void addGlobalParameterWithEmptyOptions() { + Map options = [:] + String path = '/path/' + List expected = [[path: path] + options] + + def result = ParameterStoreBuildWrapperPlugin.withGlobalParameter(path, options) + + assertEquals(expected, result.globalParameterOptions) + } + + @Test + void addGlobalParameterWithOptions() { + Map options = [recursive: true, basename: 'relative'] + String path = '/path/' + List expected = [[path: path] + options] + + def result = ParameterStoreBuildWrapperPlugin.withGlobalParameter(path, options) + + assertEquals(expected, result.globalParameterOptions) + } + + @Test + void addMulitpleGlobalParameters() { + List expected = [] + def result = ParameterStoreBuildWrapperPlugin.withGlobalParameter('/path/') + .withGlobalParameter('/path2/', [:]) + .withGlobalParameter('/path3/', [recursive: true]) + .withGlobalParameter('/path4/', [basename: 'something']) + expected << [path:'/path/'] + expected << [path:'/path2/'] + expected << [path: '/path3/', recursive: true] + expected << [path: '/path4/', basename: 'something'] + assertEquals(expected, result.globalParameterOptions) + } + } }