diff --git a/build.gradle b/build.gradle index 215ea39..84757ec 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,8 @@ import com.google.common.io.Files // Settings ext { project_group = 'com.github.blacklocus' - project_version = '0.1.10' - project_jdk = '1.7' + project_version = '0.2.0' + project_jdk = '1.8' project_pom = { name 'rds-echo' description 'Tooling for automated RDS restorations from snapshots' @@ -93,6 +93,7 @@ apply plugin: 'maven' apply plugin: 'signing' apply plugin: 'idea' + buildscript { repositories { mavenCentral() @@ -100,9 +101,12 @@ buildscript { dependencies { classpath 'com.github.rholder:gradle-autojar:1.0.1' } + } apply plugin: 'gradle-autojar' +mainClassName = "com.github.blacklocus.rdsecho.Echo" + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Dependencies @@ -121,14 +125,13 @@ repositories { } dependencies { - compile 'com.amazonaws:aws-java-sdk-rds:1.9.16' - compile 'com.amazonaws:aws-java-sdk-route53:1.9.16' + compile 'com.amazonaws:aws-java-sdk-rds:1.11.827' + compile 'com.amazonaws:aws-java-sdk-route53:1.11.827' compile 'commons-configuration:commons-configuration:1.10' compile 'com.google.guava:guava:18.0' compile 'org.slf4j:slf4j-api:1.7.10' compile 'org.slf4j:slf4j-simple:1.7.10' compile 'com.github.rholder:guava-retrying:2.0.0' - testCompile 'org.testng:testng:6.8.17' // Because can output line numbers testCompile 'ch.qos.logback:logback-core:1.1.2' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0087cd3..758de96 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b01442f..2d80b69 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jan 23 18:28:15 UTC 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew index 91a7e26..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/github/blacklocus/rdsecho/AbstractEchoIntermediateStage.java b/src/main/java/com/github/blacklocus/rdsecho/AbstractEchoIntermediateStage.java index f8cf8a1..d5b7d52 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/AbstractEchoIntermediateStage.java +++ b/src/main/java/com/github/blacklocus/rdsecho/AbstractEchoIntermediateStage.java @@ -24,7 +24,7 @@ package com.github.blacklocus.rdsecho; import com.amazonaws.services.rds.AmazonRDS; -import com.amazonaws.services.rds.AmazonRDSClient; +import com.amazonaws.services.rds.AmazonRDSClientBuilder; import com.amazonaws.services.rds.model.AddTagsToResourceRequest; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.Tag; @@ -43,7 +43,7 @@ abstract class AbstractEchoIntermediateStage implements Callable { final String requisiteStage; final String resultantStage; - final AmazonRDS rds = new AmazonRDSClient(); + final AmazonRDS rds = AmazonRDSClientBuilder.defaultClient(); final EchoCfg cfg = EchoCfg.getInstance(); final EchoUtil echo = new EchoUtil(); diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoCfg.java b/src/main/java/com/github/blacklocus/rdsecho/EchoCfg.java index 3489952..ee8514d 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoCfg.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoCfg.java @@ -25,13 +25,18 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; import org.apache.commons.configuration.CompositeConfiguration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.configuration.SystemConfiguration; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Arrays; +import java.util.Collections; + public class EchoCfg { private static final Logger LOG = LoggerFactory.getLogger(EchoCfg.class); @@ -56,12 +61,17 @@ public class EchoCfg { public static final String PROP_NEW_OPTION_GROUP_NAME = PREFIX + "new.optionGroupName"; public static final String PROP_NEW_AUTO_MINOR_VERSION_UPGRADE = PREFIX + "new.autoMinorVersionUpgrade"; public static final String PROP_NEW_TAGS = PREFIX + "new.tags"; + public static final String PROP_NEW_VPC_SECURITY_GROUP_IDS = PREFIX + "new.vpcSecurityGroupIds"; + public static final String PROP_NEW_DB_SUBNET_GROUP_NAME = PREFIX + "new.dbSubnetGroupName"; + public static final String PROP_NEW_MINIMUM_AGE_HOURS = PREFIX + "new.minimumAgeHours"; // Modify parameters are mostly optional public static final String PROP_MOD_DB_PARAMETER_GROUP_NAME = PREFIX + "mod.dbParameterGroupName"; public static final String PROP_MOD_DB_SECURITY_GROUPS = PREFIX + "mod.dbSecurityGroups"; + public static final String PROP_MOD_VPC_SECURITY_GROUP_IDS = PREFIX + "mod.vpcSecurityGroupIds"; public static final String PROP_MOD_BACKUP_RETENTION_PERIOD = PREFIX + "mod.backupRetentionPeriod"; public static final String PROP_MOD_APPLY_IMMEDIATELY = PREFIX + "mod.applyImmediately"; + public static final String PROP_MOD_DB_SUBNET_GROUP_NAME = PREFIX + "mod.dbSubnetGroupName"; // Promote parameters are required public static final String PROP_PROMOTE_CNAME = PREFIX + "promote.cname"; @@ -169,12 +179,41 @@ public Optional newTags() { } } + public Optional newVpcSecurityGroupIds() { + String[] valuesRead = cfg.getStringArray(PROP_NEW_VPC_SECURITY_GROUP_IDS); + String[] values = Arrays.stream(valuesRead).filter(StringUtils::isNotEmpty).toArray(String[]::new); + if (values == null || values.length == 0) { + return Optional.absent(); + } else { + return Optional.of(values); + } + } + + public Optional newDbSubnetGroupName() { + return Optional.fromNullable(cfg.getString(PROP_NEW_DB_SUBNET_GROUP_NAME, null)); + } + + public Optional newMinimumAgeHours() { + return Optional.fromNullable(cfg.getInteger(PROP_NEW_MINIMUM_AGE_HOURS, 20)); + } + public Optional modDbParameterGroupName() { return Optional.fromNullable(cfg.getString(PROP_MOD_DB_PARAMETER_GROUP_NAME)); } public Optional modDbSecurityGroups() { - String[] values = cfg.getStringArray(PROP_MOD_DB_SECURITY_GROUPS); + String[] valuesRead = cfg.getStringArray(PROP_MOD_DB_SECURITY_GROUPS); + String[] values = Arrays.stream(valuesRead).filter(StringUtils::isNotEmpty).toArray(String[]::new); + if (values == null || values.length == 0) { + return Optional.absent(); + } else { + return Optional.of(values); + } + } + + public Optional modVpcSecurityGroupIds() { + String[] valuesRead = cfg.getStringArray(PROP_MOD_VPC_SECURITY_GROUP_IDS); + String[] values = Arrays.stream(valuesRead).filter(StringUtils::isNotEmpty).toArray(String[]::new); if (values == null || values.length == 0) { return Optional.absent(); } else { @@ -190,6 +229,10 @@ public boolean modApplyImmediately() { return cfg.getBoolean(PROP_MOD_APPLY_IMMEDIATELY); } + public Optional modDbSubnetGroupName() { + return Optional.fromNullable(cfg.getString(PROP_MOD_DB_SUBNET_GROUP_NAME, null)); + } + public String promoteCname() { return cfg.getString(PROP_PROMOTE_CNAME); } diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoModify.java b/src/main/java/com/github/blacklocus/rdsecho/EchoModify.java index dc0288e..c9d82fa 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoModify.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoModify.java @@ -65,11 +65,22 @@ boolean traverseStage(DBInstance instance) { request.withDBSecurityGroups(dbSecurityGroupsOpt.get()); printer.format(" db security groups : %s%n", Arrays.asList(dbSecurityGroupsOpt.get())); } + Optional vpcSecurityGroupIdsOpt = cfg.modVpcSecurityGroupIds(); + if (vpcSecurityGroupIdsOpt.isPresent()) { + request.withVpcSecurityGroupIds(vpcSecurityGroupIdsOpt.get()); + printer.format(" vpc security group ids : %s%n", Arrays.asList(vpcSecurityGroupIdsOpt.get())); + } + Optional dbSubnetGroupNameOpt = cfg.modDbSubnetGroupName(); + if (dbSubnetGroupNameOpt.isPresent()) { + request.setDBSubnetGroupName(dbSubnetGroupNameOpt.get()); + printer.format(" db subnet group name : %s%n", dbSubnetGroupNameOpt.get()); + } Optional backupRetentionPeriodOpt = cfg.modBackupRetentionPeriod(); if (backupRetentionPeriodOpt.isPresent()) { request.withBackupRetentionPeriod(backupRetentionPeriodOpt.get()); printer.format(" backup retention period: %d%n", backupRetentionPeriodOpt.get()); } + boolean applyImmediately = cfg.modApplyImmediately(); printer.format(" apply immediately : %b%n", applyImmediately); request.withApplyImmediately(applyImmediately); diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoNew.java b/src/main/java/com/github/blacklocus/rdsecho/EchoNew.java index f5f1155..19d74c4 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoNew.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoNew.java @@ -26,6 +26,7 @@ import com.amazonaws.services.rds.AmazonRDS; import com.amazonaws.services.rds.AmazonRDSClient; +import com.amazonaws.services.rds.AmazonRDSClientBuilder; import com.amazonaws.services.rds.model.AddTagsToResourceRequest; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.DBSnapshot; @@ -50,7 +51,7 @@ public class EchoNew implements Callable { private static final Logger LOG = LoggerFactory.getLogger(EchoNew.class); - final AmazonRDS rds = new AmazonRDSClient(); + final AmazonRDS rds = AmazonRDSClientBuilder.defaultClient(); final EchoCfg cfg = EchoCfg.getInstance(); final EchoUtil echo = new EchoUtil(); @@ -62,19 +63,22 @@ public Boolean call() throws Exception { String tagEchoManaged = echo.getTagEchoManaged(); - LOG.info("[{}] Checking to see if current echo-created instance (tagged {}) was created less than 24 hours ago. " + + Optional minimumAgeHoursOpt = cfg.newMinimumAgeHours(); + int minimumAgeHours = minimumAgeHoursOpt.isPresent()? minimumAgeHoursOpt.get(): 24; + + LOG.info("[{}] Checking to see if current echo-created instance (tagged {}) was created less than x hours ago. " + "If so this operation will not continue.", COMMAND_NEW, tagEchoManaged); Optional newestInstanceOpt = echo.lastEchoInstance(); if (newestInstanceOpt.isPresent()) { - if (new DateTime(newestInstanceOpt.get().getInstanceCreateTime()).plusHours(24).isAfter(DateTime.now())) { - LOG.info("[{}] Last echo-created RDS instance {} was created less than 24 hours ago. Aborting.", - COMMAND_NEW, tagEchoManaged); + if (new DateTime(newestInstanceOpt.get().getInstanceCreateTime()).plusHours(minimumAgeHours).isAfter(DateTime.now())) { + LOG.info("[{}] Last echo-created RDS instance {} was created less than {} hours ago. Aborting.", + COMMAND_NEW, tagEchoManaged, minimumAgeHours); return false; } else { - LOG.info("[{}] Last echo-created RDS instance {} was created more than 24 hours ago. Proceeding.", - COMMAND_NEW, tagEchoManaged); + LOG.info("[{}] Last echo-created RDS instance {} was created more than {} hours ago. Proceeding.", + COMMAND_NEW, tagEchoManaged, minimumAgeHours); } } else { @@ -175,6 +179,18 @@ public Boolean call() throws Exception { printer.format(" auto minor ver up: %s%n", autoMinorVersionOpt.get()); } + Optional vpcSecurityGroupIdsOpt = cfg.newVpcSecurityGroupIds(); + if (vpcSecurityGroupIdsOpt.isPresent()) { + request.withVpcSecurityGroupIds(vpcSecurityGroupIdsOpt.get()); + printer.format(" vpc security group ids: %s%n", Arrays.asList(vpcSecurityGroupIdsOpt.get())); + } + + Optional dbSubnetGroupNameOpt = cfg.newDbSubnetGroupName(); + if (dbSubnetGroupNameOpt.isPresent()) { + request.withDBSubnetGroupName(dbSubnetGroupNameOpt.get()); + printer.format(" db subnet group name: %s%n", dbSubnetGroupNameOpt.get()); + } + LOG.info(proposed.toString()); // Interactive user confirmation diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoPromote.java b/src/main/java/com/github/blacklocus/rdsecho/EchoPromote.java index 31407b5..d144773 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoPromote.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoPromote.java @@ -23,12 +23,10 @@ */ package com.github.blacklocus.rdsecho; -import com.amazonaws.services.rds.model.AddTagsToResourceRequest; -import com.amazonaws.services.rds.model.DBInstance; -import com.amazonaws.services.rds.model.Endpoint; -import com.amazonaws.services.rds.model.Tag; +import com.amazonaws.services.rds.model.*; import com.amazonaws.services.route53.AmazonRoute53; import com.amazonaws.services.route53.AmazonRoute53Client; +import com.amazonaws.services.route53.AmazonRoute53ClientBuilder; import com.amazonaws.services.route53.model.Change; import com.amazonaws.services.route53.model.ChangeAction; import com.amazonaws.services.route53.model.ChangeBatch; @@ -41,9 +39,11 @@ import com.github.blacklocus.rdsecho.utl.RdsFind; import com.github.blacklocus.rdsecho.utl.Route53Find; import com.google.common.base.Optional; +import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -55,8 +55,8 @@ public class EchoPromote extends AbstractEchoIntermediateStage { private static final Logger LOG = LoggerFactory.getLogger(EchoPromote.class); - final AmazonRoute53 route53 = new AmazonRoute53Client(); - final Route53Find route53Find = new Route53Find(); + final AmazonRoute53 route53 = AmazonRoute53ClientBuilder.defaultClient(); + final Route53Find route53Find = new Route53Find(route53); public EchoPromote() { super(EchoConst.STAGE_REBOOTED, EchoConst.STAGE_PROMOTED); @@ -70,10 +70,8 @@ boolean traverseStage(DBInstance instance) { HostedZone hostedZone = route53Find.hostedZone(nameEquals(tld)).get(); LOG.info("[{}] Found corresponding HostedZone. name: {} id: {}", getCommand(), hostedZone.getName(), hostedZone.getId()); - ResourceRecordSet resourceRecordSet = route53Find.resourceRecordSet( - hostedZone.getId(), cnameEquals(cfg.promoteCname())).get(); - ResourceRecord resourceRecord = getOnlyElement(resourceRecordSet.getResourceRecords()); - LOG.info("[{}] Found CNAME {} with current value {}", getCommand(), resourceRecordSet.getName(), resourceRecord.getValue()); + Optional resourceRecordSetOpt = route53Find.resourceRecordSet( + hostedZone.getId(), cnameEquals(cfg.promoteCname())); Endpoint endpoint = instance.getEndpoint(); String tagEchoManaged = echo.getTagEchoManaged(); @@ -84,13 +82,35 @@ boolean traverseStage(DBInstance instance) { return false; } String instanceAddr = endpoint.getAddress(); + + if( !resourceRecordSetOpt.isPresent() ) { + ResourceRecordSet recordSet = new ResourceRecordSet(cfg.promoteCname(), "CNAME"); + + ResourceRecord record = new ResourceRecord(instanceAddr + ".notexist"); + recordSet.setResourceRecords(Lists.newArrayList(record)); + + resourceRecordSetOpt = Optional.of(recordSet); + } + + ResourceRecordSet resourceRecordSet = resourceRecordSetOpt.get(); + ResourceRecord resourceRecord = getOnlyElement(resourceRecordSet.getResourceRecords()); + LOG.info("[{}] Found CNAME {} with current value {}", getCommand(), resourceRecordSet.getName(), resourceRecord.getValue()); + if (resourceRecord.getValue().equals(instanceAddr)) { LOG.info("[{}] Echo DB instance {} ({}) lines up with CNAME {}. Nothing to do.", getCommand(), tagEchoManaged, instanceAddr, resourceRecordSet.getName()); - return false; } else { LOG.info("[{}] Echo DB instance {} ({}) differs from CNAME {}.", getCommand(), tagEchoManaged, instanceAddr, resourceRecordSet.getName()); + + LOG.info("[{}] Updating CNAME {} from {} to {}", getCommand(), cfg.name(), resourceRecord.getValue(), instanceAddr); + ChangeResourceRecordSetsRequest request = new ChangeResourceRecordSetsRequest() + .withHostedZoneId(hostedZone.getId()) + .withChangeBatch(new ChangeBatch() + .withChanges(new Change(ChangeAction.UPSERT, new ResourceRecordSet(cfg.promoteCname(), RRType.CNAME) + .withResourceRecords(new ResourceRecord(instanceAddr)) + .withTTL(cfg.promoteTtl())))); + route53.changeResourceRecordSets(request); } if (cfg.interactive()) { @@ -101,15 +121,6 @@ boolean traverseStage(DBInstance instance) { } } - LOG.info("[{}] Updating CNAME {} from {} to {}", getCommand(), cfg.name(), resourceRecord.getValue(), instanceAddr); - ChangeResourceRecordSetsRequest request = new ChangeResourceRecordSetsRequest() - .withHostedZoneId(hostedZone.getId()) - .withChangeBatch(new ChangeBatch() - .withChanges(new Change(ChangeAction.UPSERT, new ResourceRecordSet(cfg.promoteCname(), RRType.CNAME) - .withResourceRecords(new ResourceRecord(instanceAddr)) - .withTTL(cfg.promoteTtl())))); - route53.changeResourceRecordSets(request); - Optional promoteTags = cfg.promoteTags(); if (promoteTags.isPresent()) { List tags = EchoUtil.parseTags(promoteTags.get()); @@ -125,6 +136,21 @@ boolean traverseStage(DBInstance instance) { LOG.info("[{}] Searching for any existing promoted instance to demote.", getCommand()); // TODO no it doesn't + EchoUtil echo = new EchoUtil(); + + Iterable validInstances = echo.echoInstances(); + String tagEchoStage = echo.getTagEchoStage(); + + for (DBInstance validInstance : validInstances) { + if (validInstance.getDBInstanceArn().equalsIgnoreCase(instance.getDBInstanceArn())) { + continue; + } + + rds.addTagsToResource(new AddTagsToResourceRequest() + .withResourceName(RdsFind.instanceArn(cfg.region(), cfg.accountNumber(), validInstance.getDBInstanceIdentifier())) + .withTags(new Tag().withKey(tagEchoStage).withValue(EchoConst.STAGE_FORGOTTEN))); + } + return true; } diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoRetire.java b/src/main/java/com/github/blacklocus/rdsecho/EchoRetire.java index 088eb0d..9480954 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoRetire.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoRetire.java @@ -23,9 +23,13 @@ */ package com.github.blacklocus.rdsecho; +import com.amazonaws.services.rds.model.AddTagsToResourceRequest; import com.amazonaws.services.rds.model.DBInstance; import com.amazonaws.services.rds.model.DeleteDBInstanceRequest; +import com.amazonaws.services.rds.model.Tag; import com.github.blacklocus.rdsecho.utl.EchoUtil; +import com.github.blacklocus.rdsecho.utl.RdsFind; +import com.google.common.base.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,9 +38,62 @@ public class EchoRetire extends AbstractEchoIntermediateStage { private static final Logger LOG = LoggerFactory.getLogger(EchoRetire.class); public EchoRetire() { - super(EchoConst.STAGE_PROMOTED, EchoConst.STAGE_RETIRED); + super(EchoConst.STAGE_FORGOTTEN, EchoConst.STAGE_RETIRED); } + @Override + public Boolean call() throws Exception { + + // Validate state, make sure we're operating on what we expect to. + + String tagEchoManaged = echo.getTagEchoManaged(); + String tagEchoStage = echo.getTagEchoStage(); + String command = this.getCommand(); + + LOG.info("[{}] Locating Echo managed instances (tagged with {}=true)", command, tagEchoManaged); + + Iterable instances = echo.echoInstances(); + for (DBInstance instance : instances) { + String dbInstanceId = instance.getDBInstanceIdentifier(); + LOG.info("[{}] Located echo-managed instance with identifier {}", command, dbInstanceId); + + Optional stageOpt = echo.instanceStage(instance.getDBInstanceIdentifier()); + if (!stageOpt.isPresent()) { + LOG.error("[{}] Unable to read Echo stage tag on instance {}. Exiting.\n" + + "(If the instance is supposed to be in stage {} but isn't, edit " + + "the instance's tags to add {}={} and run this operation again.)", + command, dbInstanceId, requisiteStage, tagEchoStage, requisiteStage); + continue; + } + String instanceStage = stageOpt.get().getValue(); + if (!requisiteStage.equals(instanceStage)) { + LOG.info("[{}] Instance {} has stage {} but this operation is looking for {}={}. Exiting.\n", + command, dbInstanceId, instanceStage, tagEchoStage, requisiteStage); + continue; + } + + // Looks like we found a good echo instance, but is it available to us? + + if (!"available".equals(instance.getDBInstanceStatus())) { + LOG.info("[{}] Instance {} is in correct stage of {} but does not have status 'available' (saw {}) so aborting.", + command, dbInstanceId, instanceStage, instance.getDBInstanceStatus()); + continue; + } + + // Do the part special to traversing this stage + + if (traverseStage(instance)) { + // Advance. This replaces, same-named tags. + rds.addTagsToResource(new AddTagsToResourceRequest() + .withResourceName(RdsFind.instanceArn(cfg.region(), cfg.accountNumber(), instance.getDBInstanceIdentifier())) + .withTags(new Tag().withKey(tagEchoStage).withValue(resultantStage))); + } + } + + return true; + } + + @Override boolean traverseStage(DBInstance instance) { diff --git a/src/main/java/com/github/blacklocus/rdsecho/EchoSampleOpts.java b/src/main/java/com/github/blacklocus/rdsecho/EchoSampleOpts.java index 17be596..25d8b50 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/EchoSampleOpts.java +++ b/src/main/java/com/github/blacklocus/rdsecho/EchoSampleOpts.java @@ -1,3 +1,26 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 BlackLocus + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.github.blacklocus.rdsecho; import org.apache.commons.configuration.CompositeConfiguration; diff --git a/src/main/java/com/github/blacklocus/rdsecho/utl/EchoUtil.java b/src/main/java/com/github/blacklocus/rdsecho/utl/EchoUtil.java index 46f34c0..decb5f6 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/utl/EchoUtil.java +++ b/src/main/java/com/github/blacklocus/rdsecho/utl/EchoUtil.java @@ -29,7 +29,6 @@ import com.github.blacklocus.rdsecho.EchoCfg; import com.github.blacklocus.rdsecho.EchoConst; import com.google.common.base.Optional; -import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,10 +59,9 @@ public Optional lastEchoInstance() { cfg.region(), cfg.accountNumber(), getTagEchoManaged(), "true"))); } - public Optional promotedInstance() { - return Optional.fromNullable(Iterables.getOnlyElement(rdsFind.instances(rdsFind.instanceHasTag( - cfg.region(), cfg.accountNumber(), getTagEchoStage(), EchoConst.STAGE_PROMOTED - )), null)); + public Iterable echoInstances() { + return RdsFind.validInstances(rdsFind.instances(rdsFind.instanceHasTag( + cfg.region(), cfg.accountNumber(), getTagEchoManaged(), "true"))); } public Optional instanceStage(String dbInstanceIdentifier) { diff --git a/src/main/java/com/github/blacklocus/rdsecho/utl/PagingIterable.java b/src/main/java/com/github/blacklocus/rdsecho/utl/PagingIterable.java index 3fc6531..3df6e3d 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/utl/PagingIterable.java +++ b/src/main/java/com/github/blacklocus/rdsecho/utl/PagingIterable.java @@ -24,7 +24,7 @@ package com.github.blacklocus.rdsecho.utl; import com.google.common.base.Supplier; -import org.apache.http.annotation.NotThreadSafe; +import com.amazonaws.annotation.NotThreadSafe; import java.util.Iterator; diff --git a/src/main/java/com/github/blacklocus/rdsecho/utl/RdsFind.java b/src/main/java/com/github/blacklocus/rdsecho/utl/RdsFind.java index 2e041d1..cc10277 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/utl/RdsFind.java +++ b/src/main/java/com/github/blacklocus/rdsecho/utl/RdsFind.java @@ -25,16 +25,8 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.services.rds.AmazonRDS; -import com.amazonaws.services.rds.AmazonRDSClient; -import com.amazonaws.services.rds.model.DBInstance; -import com.amazonaws.services.rds.model.DBSnapshot; -import com.amazonaws.services.rds.model.DescribeDBInstancesRequest; -import com.amazonaws.services.rds.model.DescribeDBInstancesResult; -import com.amazonaws.services.rds.model.DescribeDBSnapshotsRequest; -import com.amazonaws.services.rds.model.DescribeDBSnapshotsResult; -import com.amazonaws.services.rds.model.ListTagsForResourceRequest; -import com.amazonaws.services.rds.model.ListTagsForResourceResult; -import com.amazonaws.services.rds.model.Tag; +import com.amazonaws.services.rds.AmazonRDSClientBuilder; +import com.amazonaws.services.rds.model.*; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; @@ -43,16 +35,19 @@ import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import javax.annotation.Nullable; import java.util.Collections; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class RdsFind { - final AmazonRDS rds = new AmazonRDSClient(); + final AmazonRDS rds = AmazonRDSClientBuilder.defaultClient(); // Retry 10 times with exponential backoff, starting with 1 second bounded to 60 seconds final Retryer tagRetryer = RetryerBuilder.newBuilder() @@ -192,6 +187,15 @@ public boolean apply(@Nullable DBInstance input) { return Optional.fromNullable(newest); } + public static List validInstances(Iterable instances) { + List dbInstances = Lists.newArrayList(instances); + return dbInstances + .stream() + .filter(input -> input != null && input.getInstanceCreateTime() != null ) + .collect(Collectors.toList()) + ; + } + public static Optional newestSnapshot(Iterable snapshots) { DBSnapshot newest = null; diff --git a/src/main/java/com/github/blacklocus/rdsecho/utl/Route53Find.java b/src/main/java/com/github/blacklocus/rdsecho/utl/Route53Find.java index e23c4a9..e8f5d52 100644 --- a/src/main/java/com/github/blacklocus/rdsecho/utl/Route53Find.java +++ b/src/main/java/com/github/blacklocus/rdsecho/utl/Route53Find.java @@ -41,7 +41,11 @@ public class Route53Find { - final AmazonRoute53 route53 = new AmazonRoute53Client(); + final AmazonRoute53 route53; + + public Route53Find(AmazonRoute53 route53) { + this.route53 = route53; + } public Optional hostedZone() { return Optional.fromNullable(Iterables.getFirst(hostedZones(), null)); diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties index ec4f448..6a0919d 100644 --- a/src/main/resources/simplelogger.properties +++ b/src/main/resources/simplelogger.properties @@ -1,3 +1,27 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2015 BlackLocus +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + org.slf4j.simpleLogger.defaultLogLevel=info org.slf4j.simpleLogger.showDateTime=true org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss Z diff --git a/src/test/java/com/github/blacklocus/rdsecho/EchoCfgTest.java b/src/test/java/com/github/blacklocus/rdsecho/EchoCfgTest.java index 8301fb7..dbdf0b4 100644 --- a/src/test/java/com/github/blacklocus/rdsecho/EchoCfgTest.java +++ b/src/test/java/com/github/blacklocus/rdsecho/EchoCfgTest.java @@ -1,3 +1,26 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 BlackLocus + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.github.blacklocus.rdsecho; import com.google.common.base.Optional; diff --git a/src/test/java/com/github/blacklocus/rdsecho/utl/EchoUtilTest.java b/src/test/java/com/github/blacklocus/rdsecho/utl/EchoUtilTest.java index db89644..4fa110c 100644 --- a/src/test/java/com/github/blacklocus/rdsecho/utl/EchoUtilTest.java +++ b/src/test/java/com/github/blacklocus/rdsecho/utl/EchoUtilTest.java @@ -1,3 +1,26 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015 BlackLocus + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.github.blacklocus.rdsecho.utl; import com.amazonaws.services.rds.model.Tag;