Skip to content

Instantly share code, notes, and snippets.

@re-mscho
Last active June 24, 2020 08:41
Show Gist options
  • Save re-mscho/5c5bd44330d38e95965b523b55db1743 to your computer and use it in GitHub Desktop.
Save re-mscho/5c5bd44330d38e95965b523b55db1743 to your computer and use it in GitHub Desktop.
#!/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