diff --git a/gradle.properties b/gradle.properties index bffd84c4..e6587d64 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,4 @@ group=com.nike artifactId=cerberus-lifecycle-cli -version=4.4.0 +version=4.5.0 diff --git a/src/main/java/com/nike/cerberus/cli/CerberusRunner.java b/src/main/java/com/nike/cerberus/cli/CerberusRunner.java index e0455719..58dae884 100644 --- a/src/main/java/com/nike/cerberus/cli/CerberusRunner.java +++ b/src/main/java/com/nike/cerberus/cli/CerberusRunner.java @@ -41,7 +41,8 @@ import com.nike.cerberus.command.composite.PrintAllStackInformationCommand; import com.nike.cerberus.command.certificates.RotateCertificatesCommand; import com.nike.cerberus.command.core.InitializeEnvironmentCommand; -import com.nike.cerberus.command.core.UpdateAllStackTagsCommand; +import com.nike.cerberus.command.core.SyncConfigCommand; +import com.nike.cerberus.command.composite.UpdateAllStackTagsCommand; import com.nike.cerberus.command.rds.CleanUpRdsSnapshotsCommand; import com.nike.cerberus.command.rds.CopyRdsSnapshotsCommand; import com.nike.cerberus.command.rds.CreateDatabaseCommand; @@ -235,6 +236,7 @@ private void registerAllCommands() { registerCommand(new EnableAuditLoggingForExistingEnvironmentCommand()); registerCommand(new UpdateStackTagsCommand()); registerCommand(new UpdateAllStackTagsCommand()); + registerCommand(new SyncConfigCommand()); } /** diff --git a/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java b/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java index 8ff36d50..eda17e92 100644 --- a/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java +++ b/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java @@ -25,7 +25,7 @@ import com.nike.cerberus.command.certificates.GenerateAndRotateCertificatesCommand; import com.nike.cerberus.command.certificates.RotateCertificatesCommand; import com.nike.cerberus.command.core.InitializeEnvironmentCommand; -import com.nike.cerberus.command.core.UpdateAllStackTagsCommand; +import com.nike.cerberus.command.composite.UpdateAllStackTagsCommand; import com.nike.cerberus.command.rds.CreateDatabaseCommand; import com.nike.cerberus.command.core.CreateEdgeDomainRecordCommand; import com.nike.cerberus.command.core.CreateLoadBalancerCommand; diff --git a/src/main/java/com/nike/cerberus/command/core/UpdateAllStackTagsCommand.java b/src/main/java/com/nike/cerberus/command/composite/UpdateAllStackTagsCommand.java similarity index 92% rename from src/main/java/com/nike/cerberus/command/core/UpdateAllStackTagsCommand.java rename to src/main/java/com/nike/cerberus/command/composite/UpdateAllStackTagsCommand.java index 6518ea78..46df25c4 100644 --- a/src/main/java/com/nike/cerberus/command/core/UpdateAllStackTagsCommand.java +++ b/src/main/java/com/nike/cerberus/command/composite/UpdateAllStackTagsCommand.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.nike.cerberus.command.core; +package com.nike.cerberus.command.composite; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; @@ -23,7 +23,7 @@ import com.nike.cerberus.operation.Operation; import com.nike.cerberus.operation.composite.UpdateAllStackTagsOperation; -import static com.nike.cerberus.command.core.UpdateAllStackTagsCommand.COMMAND_NAME; +import static com.nike.cerberus.command.composite.UpdateAllStackTagsCommand.COMMAND_NAME; /** diff --git a/src/main/java/com/nike/cerberus/command/core/SyncConfigCommand.java b/src/main/java/com/nike/cerberus/command/core/SyncConfigCommand.java new file mode 100644 index 00000000..2a613059 --- /dev/null +++ b/src/main/java/com/nike/cerberus/command/core/SyncConfigCommand.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.command.core; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.nike.cerberus.command.Command; +import com.nike.cerberus.operation.Operation; +import com.nike.cerberus.operation.core.SyncConfigOperation; + +import static com.nike.cerberus.command.core.UpdateStackCommand.COMMAND_NAME; + + +/** + * Command for syncing configs between regions. + */ +@Parameters(commandNames = COMMAND_NAME, commandDescription = "Syncs configs between regions. Recursively copies all files from the source region's config bucket (as defined in --region) to the destination region's config bucket.") +public class SyncConfigCommand implements Command { + + public static final String COMMAND_NAME = "sync-config"; + public static final String DESTINATION_REGION_LONG_ARG = "--destination-region"; + public static final String DRY_LONG_ARG = "--dry"; + public static final String ALL_LONG_ARG = "--all"; + + @Parameter(names = {DESTINATION_REGION_LONG_ARG}, description = "The destination region") + private String destinationRegionName; + + @Parameter(names = {DRY_LONG_ARG}, description = "Displays destination buckets and files to be copied over without actually running them") + private boolean dryrun; + + @Parameter(names = {ALL_LONG_ARG}, description = "Sync up all regions as defines in the environment data") + private boolean all; + + public String getDestinationRegionName() { + return destinationRegionName; + } + + public boolean isDryrun() { + return dryrun; + } + + public boolean isAll() { + return all; + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public Class> getOperationClass() { + return SyncConfigOperation.class; + } +} diff --git a/src/main/java/com/nike/cerberus/operation/composite/UpdateAllStackTagsOperation.java b/src/main/java/com/nike/cerberus/operation/composite/UpdateAllStackTagsOperation.java index a105c494..a9027458 100644 --- a/src/main/java/com/nike/cerberus/operation/composite/UpdateAllStackTagsOperation.java +++ b/src/main/java/com/nike/cerberus/operation/composite/UpdateAllStackTagsOperation.java @@ -16,7 +16,7 @@ package com.nike.cerberus.operation.composite; -import com.nike.cerberus.command.core.UpdateAllStackTagsCommand; +import com.nike.cerberus.command.composite.UpdateAllStackTagsCommand; import com.nike.cerberus.command.core.UpdateStackTagsCommand; import com.nike.cerberus.domain.environment.Stack; diff --git a/src/main/java/com/nike/cerberus/operation/core/SyncConfigOperation.java b/src/main/java/com/nike/cerberus/operation/core/SyncConfigOperation.java new file mode 100644 index 00000000..3c15103b --- /dev/null +++ b/src/main/java/com/nike/cerberus/operation/core/SyncConfigOperation.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.operation.core; + +import com.amazonaws.regions.Regions; +import com.nike.cerberus.command.core.SyncConfigCommand; +import com.nike.cerberus.operation.Operation; +import com.nike.cerberus.store.ConfigStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static com.nike.cerberus.module.CerberusModule.CONFIG_REGION; + +/** + * Operation for syncing configs between regions. + */ +public class SyncConfigOperation implements Operation { + + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ConfigStore configStore; + + private Regions configRegion; + + @Inject + public SyncConfigOperation(ConfigStore configStore, @Named(CONFIG_REGION) String configRegion) { + this.configStore = configStore; + this.configRegion = Regions.fromName(configRegion); + } + + @Override + public void run(SyncConfigCommand command) { + List destinationRegions = command.isAll() ? configStore.getSyncDestinationRegions() : Arrays.asList(Regions.fromName(command.getDestinationRegionName())); + + if (command.isDryrun()){ + logger.info("Destination buckets: {}", destinationRegions.stream() + .map(region -> configStore.getConfigBucketForRegion(region)).collect(Collectors.joining(", "))); + logger.info("Files to be copied over:"); + configStore.listKeys().forEach(key -> logger.info(key)); + } else { + for (Regions region: destinationRegions) { + logger.info("Destination bucket: {}", configStore.getConfigBucketForRegion(region)); + configStore.sync(region); + } + } + + } + + @Override + public boolean isRunnable(SyncConfigCommand command) { + boolean isRunnable = true; + if (!command.isAll() && configRegion.equals(Regions.fromName(command.getDestinationRegionName()))){ + isRunnable = false; + logger.error("The source and destination region must be different."); + } + return isRunnable; + } +} \ No newline at end of file diff --git a/src/main/java/com/nike/cerberus/service/S3StoreService.java b/src/main/java/com/nike/cerberus/service/S3StoreService.java index c136b182..d70139e4 100644 --- a/src/main/java/com/nike/cerberus/service/S3StoreService.java +++ b/src/main/java/com/nike/cerberus/service/S3StoreService.java @@ -170,4 +170,10 @@ public Optional getHash(String path) { logger.debug("The hash for {} is {}", path, objectMetadata.getETag().toString()); return Optional.ofNullable(objectMetadata.getETag()); } + + @Override + public void copyFrom(String sourceBucketName, String sourceS3key) { + logger.info("Copying {}", sourceS3key); + s3Client.copyObject(sourceBucketName, sourceS3key, s3Bucket, sourceS3key); + } } diff --git a/src/main/java/com/nike/cerberus/service/StoreService.java b/src/main/java/com/nike/cerberus/service/StoreService.java index 625c57e3..5f5e0e78 100644 --- a/src/main/java/com/nike/cerberus/service/StoreService.java +++ b/src/main/java/com/nike/cerberus/service/StoreService.java @@ -38,4 +38,6 @@ public interface StoreService { void deleteAllKeysOnPartialPath(String path); Optional getHash(String path); + + void copyFrom(String sourceIdentifier, String path); } diff --git a/src/main/java/com/nike/cerberus/store/ConfigStore.java b/src/main/java/com/nike/cerberus/store/ConfigStore.java index ea502b5b..61364200 100644 --- a/src/main/java/com/nike/cerberus/store/ConfigStore.java +++ b/src/main/java/com/nike/cerberus/store/ConfigStore.java @@ -659,6 +659,12 @@ public Regions getPrimaryRegion() { return getDecryptedEnvironmentData().getPrimaryRegion(); } + public List getSyncDestinationRegions() { + List regions = getDecryptedEnvironmentData().getConfigRegions(); + regions.remove(configRegion); + return regions; + } + public List getConfigEnabledRegions() { return getDecryptedEnvironmentData().getConfigRegions(); } @@ -757,7 +763,7 @@ public boolean isConfigSynchronized(){ StoreService storeService = getStoreServiceForRegion(currentRegion, environmentData); // null hash values are treated as if they're equal - Map s3KeyToHashValueMap = Maps.uniqueIndex(storeService.getKeysInPartialPath(""), s -> storeService.getHash(s).toString()); + Map s3KeyToHashValueMap = Maps.asMap(storeService.getKeysInPartialPath(""), key -> storeService.getHash(key).toString()); if (firstRegion == null){ firstS3KeyToHashValueMap = s3KeyToHashValueMap; firstRegion = currentRegion; @@ -769,4 +775,25 @@ public boolean isConfigSynchronized(){ return result; } + + /*** + * Copy all files from config region's config bucket to destination region's config bucket + * @param destinationRegion Region to copy files to + */ + public void sync(Regions destinationRegion) { + EnvironmentData decryptedEnvironmentData = getDecryptedEnvironmentData(); + StoreService destinationStoreService = getStoreServiceForRegion(destinationRegion, decryptedEnvironmentData); + String sourceBucket = findConfigBucketInSuppliedConfigRegion(); + listKeys().forEach(k -> destinationStoreService.copyFrom(sourceBucket, k)); + } + + /*** + * List every key in the config bucket in the config region + * @return Set of keys + */ + public Set listKeys() { + StoreService storeService = getStoreServiceForRegion(configRegion, getDecryptedEnvironmentData()); + Set keys = storeService.getKeysInPartialPath(""); + return keys; + } }