Last active
June 24, 2020 08:41
-
-
Save re-mscho/5c5bd44330d38e95965b523b55db1743 to your computer and use it in GitHub Desktop.
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
#!/usr/bin/env groovy | |
@Grab('info.picocli:picocli-groovy:4.3.2') | |
import groovy.json.JsonSlurper | |
@Grab('org.yaml:snakeyaml:1.17') | |
import org.yaml.snakeyaml.Yaml | |
import picocli.CommandLine | |
import picocli.CommandLine.Help.Ansi | |
import java.util.concurrent.Callable | |
// ##################################################################################################################### | |
// Helpers | |
// ##################################################################################################################### | |
class Executor { | |
static String run(String cmd) { | |
return run([cmd]) | |
} | |
static String run(List<String> cmd) { | |
return runPiped(cmd) | |
} | |
static String runPiped(List<String>... cmds) { | |
def outputStream = new StringBuffer() | |
def proc | |
cmds.each { List<String> cmd -> | |
proc = proc ? proc.or(cmd.execute()) : cmd.execute() | |
} | |
proc.waitForProcessOutput(outputStream, System.err) | |
if (proc.exitValue() != 0) { | |
throw new Exception("Error performing ${cmds}(exit code: {${proc.exitValue()}): ${outputStream.toString()}") | |
} | |
return outputStream.toString().trim() | |
} | |
} | |
def printRecursive | |
printRecursive = { Map<String, Object> configs, Integer indent = 1 -> | |
configs.each { String k, v -> | |
if (v in Map) { | |
println ' ' * indent + "$k:" | |
printRecursive(v, indent + 1) | |
} else { | |
def vAsString | |
if (Number.isInstance(v)) { | |
vAsString = v | |
} else if (Boolean.isInstance(v)) { | |
vAsString = v.toString() | |
} else { | |
vAsString = v.toString().take(80) | |
} | |
println ' ' * indent + "$k: ${vAsString}" | |
} | |
} | |
} | |
class ConfigKeys { | |
private Map<String, Object> yamlContent | |
ConfigKeys(String stack) { | |
Yaml parser = new Yaml() | |
def config = parser.load(new File("Pulumi.${stack}.yaml").text) | |
this.yamlContent = (Map<String, Object>) config['config'] | |
} | |
void iterateRecursive(Closure callable) { | |
this.intIterateRecursive(callable, this.yamlContent) | |
} | |
private intIterateRecursive(Closure callable, Map<String, Object> content, List<String> basePath = []) { | |
content.each { String k, v -> | |
List<String> fullPath = basePath + [k] | |
if (v in Map && v.size() > 0 && !v.containsKey('secure')) { | |
this.intIterateRecursive(callable, v, fullPath) | |
} else { | |
def isSecret = false | |
if (v in Map && v.containsKey('secure')) { | |
isSecret = true | |
} | |
callable(fullPath, isSecret) | |
} | |
} | |
} | |
void writeValuesToStack(ConfigValues configValues, String stack) { | |
def currentConfigValues = ConfigValues.fromStack(stack) | |
this.iterateRecursive { List<String> path, Boolean isSecret -> | |
def oldValue = currentConfigValues.getValue(path) | |
def newValue = configValues.getValue(path) | |
if (!ConfigValues.isEqualValue(oldValue, newValue)) { | |
System.out.println(Ansi.AUTO.string("@|bold,blue ${path.join('.')} |@")) | |
println(" '${oldValue}' (${oldValue.getClass().simpleName}, old)") | |
println(" '${newValue}' (${newValue.getClass().simpleName}, new)") | |
this.writeValueToStack(path, newValue, stack, isSecret) | |
} | |
} | |
} | |
private void writeValueToStack(List<String> path, Object value, String stack, Boolean isSecret) { | |
def flag = isSecret ? '--secret' : '--plaintext' | |
List<String> cmd = ['pulumi', 'config', 'set', '--stack', stack, flag] | |
if (path.size() == 1) { | |
cmd << path.first() | |
} else { | |
cmd += ['--path', path.join('.')] | |
} | |
if (value in Map && value.isEmpty()) { | |
System.err.println( | |
Ansi.AUTO.string("@|bold,yellow WARNING: empty object is not supported right now (${path.join('.')}): ignored |@") | |
) | |
} else { | |
System.out.println( | |
Ansi.AUTO.string("@|blue executing '${cmd.join(' ')}' |@") | |
) | |
Executor.runPiped(['echo', value.toString()], cmd) | |
} | |
} | |
} | |
class ConfigValues { | |
private Map<String, Object> jsonContent | |
private ConfigValues(String jsonConfigString) { | |
def jsonSlurper = new JsonSlurper() | |
this.jsonContent = (Map<String, Object>) jsonSlurper.parseText(jsonConfigString) | |
} | |
static fromStack(String stack) { | |
def jsonConfigString = Executor.run(['pulumi', 'config', '--stack', stack, '--json', '--show-secrets']) | |
return new ConfigValues(jsonConfigString) | |
} | |
static fromFile(String filename) { | |
def jsonConfigString = new File(filename).text | |
return new ConfigValues(jsonConfigString) | |
} | |
Object getValue(List<String> path) { | |
def jsonKey = path.first() | |
Map jsonValue = (Map) this.jsonContent[jsonKey] | |
if (jsonValue == null) { | |
return null | |
} else if (jsonValue.containsKey('objectValue')) { | |
Map<String, Object> objectValue = (Map<String, Object>) jsonValue['objectValue'] | |
if (objectValue.isEmpty()) { | |
return objectValue | |
} else { | |
return this.getValueFromObject(objectValue, path.tail()) | |
} | |
} else { | |
return jsonValue['value'] | |
} | |
} | |
protected getValueFromObject(Map<String, Object> objectValue, List<String> path) { | |
if (objectValue == null) { | |
return null | |
} else if (path.size() > 1) { | |
return this.getValueFromObject( | |
(Map<String, Object>) objectValue[path.first()], | |
path.tail()) | |
} else { | |
return objectValue[path.first()] | |
} | |
} | |
static Boolean isEqualValue(Object configValue1, Object configValue2) { | |
return configValue1 == configValue2 && configValue1.getClass() == configValue2.getClass() | |
} | |
} | |
// ##################################################################################################################### | |
// Commands | |
// ##################################################################################################################### | |
@picocli.CommandLine.Command(name = 'copy', | |
mixinStandardHelpOptions = true, // add --help and --version options | |
description = "Copy config between stacks") | |
class Copy implements Callable<Integer> { | |
@CommandLine.Option(names = ["-s", "--source-stack"], required = true, | |
description = "The name of the source stack") | |
private String sourceStack | |
@CommandLine.Option(names = ["-t", "--target-stack"], required = true, | |
description = "The name of the target stack") | |
private String targetStack | |
@Override | |
Integer call() throws Exception { | |
String s = System.console().readLine('This may overwrite existing values? Continue [y/N]: ') | |
if (s != 'y') { | |
System.exit(0) | |
} | |
new ConfigKeys(sourceStack).writeValuesToStack( | |
ConfigValues.fromStack(sourceStack), | |
targetStack | |
) | |
return 0 | |
} | |
} | |
@picocli.CommandLine.Command(name = 'export', | |
mixinStandardHelpOptions = true, // add --help and --version options | |
description = "Export config values") | |
class Export implements Callable<Integer> { | |
@CommandLine.Option(names = ["-s", "--stack"], required = true, | |
description = "The name of the stack to export") | |
private String stack | |
@CommandLine.Option(names = ["-o", "--output-file"], required = true, | |
description = "The name of the output file") | |
private String outputFilename | |
@Override | |
Integer call() throws Exception { | |
def jsonConfigString = Executor.run(['pulumi', 'config', '--stack', stack, '--json', '--show-secrets']) | |
new File(outputFilename).write(jsonConfigString) | |
return 0 | |
} | |
} | |
@picocli.CommandLine.Command(name = 'import', | |
mixinStandardHelpOptions = true, // add --help and --version options | |
description = """Import config values which where previously exported from same stack using this script. | |
(Only updates to existing configuration values are supported. Adding and removing values can cause unexpected behavior.)""") | |
class Import implements Callable<Integer> { | |
@CommandLine.Option(names = ["-s", "--stack"], required = true, | |
description = "The name of the stack to export") | |
private String stack | |
@CommandLine.Option(names = ["-i", "--input-file"], required = true, | |
description = "The name of the input file") | |
private String inputFilename | |
@Override | |
Integer call() throws Exception { | |
String s = System.console().readLine('This may overwrite existing values? Continue [y/N]: ') | |
if (s != 'y') { | |
System.exit(0) | |
} | |
new ConfigKeys(stack).writeValuesToStack( | |
ConfigValues.fromFile(inputFilename), | |
stack | |
) | |
return 0 | |
} | |
} | |
@picocli.CommandLine.Command(name = 'diff', | |
mixinStandardHelpOptions = true, // add --help and --version options | |
description = """Diff config values which where previously exported from same stack using this script. | |
(Only updates to existing configuration values are supported. Added and removed values can cause unexpected behavior.)""") | |
class Diff implements Callable<Integer> { | |
@CommandLine.Option(names = ["-s", "--stack"], required = true, | |
description = "The name of the stack to export") | |
private String stack | |
@CommandLine.Option(names = ["-i", "--input-file"], required = true, | |
description = "The name of the input file") | |
private String inputFilename | |
@Override | |
Integer call() throws Exception { | |
def stackValues = ConfigValues.fromStack(stack) | |
def fileValues = ConfigValues.fromFile(inputFilename) | |
new ConfigKeys(stack).iterateRecursive { List<String> path, Boolean isSecret -> | |
def stackValue = stackValues.getValue(path) | |
def fileValue = fileValues.getValue(path) | |
if (!ConfigValues.isEqualValue(stackValue, fileValue)) { | |
System.out.println(Ansi.AUTO.string("@|bold,blue ${path.join('.')} |@")) | |
println(" '${stackValue}' (${stackValue.getClass().simpleName}, '${stack}')") | |
println(" '${fileValue}' (${fileValue.getClass().simpleName}, '${inputFilename}')") | |
} | |
} | |
return 0 | |
} | |
} | |
// ##################################################################################################################### | |
// Main | |
// ##################################################################################################################### | |
@picocli.CommandLine.Command(name = 'pulumi_config_tool.groovy', | |
version = '0.0.1', | |
mixinStandardHelpOptions = true, // add --help and --version options | |
description = "Tools for working with pulumi config files.", | |
subcommands = [Copy, Export, Import, Diff]) | |
class Main {} | |
new CommandLine(new Main()).execute(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment