diff --git a/build.gradle b/build.gradle index 1c94e202..7b8b77a8 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ allprojects { group = "com.netflix.spinnaker.swabbie" ext { - spinnakerDependenciesVersion = project.hasProperty('spinnakerDependenciesVersion') ? project.property('spinnakerDependenciesVersion') : '1.2.5' + spinnakerDependenciesVersion = project.hasProperty('spinnakerDependenciesVersion') ? project.property('spinnakerDependenciesVersion') : '1.4.0' } def checkLocalVersions = [spinnakerDependenciesVersion: spinnakerDependenciesVersion] diff --git a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaImagesUsedByInstancesProvider.kt b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaImagesUsedByInstancesProvider.kt index 2918caab..4aab5af3 100644 --- a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaImagesUsedByInstancesProvider.kt +++ b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaImagesUsedByInstancesProvider.kt @@ -20,40 +20,22 @@ import com.netflix.spinnaker.config.EddaApiClient import com.netflix.spinnaker.swabbie.* import com.netflix.spinnaker.swabbie.aws.instances.AmazonInstance import com.netflix.spinnaker.swabbie.model.WorkConfiguration -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Lazy +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component import java.lang.IllegalArgumentException import java.time.Clock -@Configuration +@Component open class EddaImagesUsedByInstancesProvider( private val clock: Clock, private val workConfigurations: List, private val instanceProvider: ResourceProvider, - private val eddaApiClients: List, - @Lazy private val imagesUsedByInstancesCache: InMemoryCache + private val eddaApiClients: List ) : CachedViewProvider { + private val log: Logger = LoggerFactory.getLogger(javaClass) - /** - * @param params["region"]: return a Set of Amazon imageIds (i.e. "ami-abc123") - * currently referenced by running EC2 instances in the region - */ - override fun getAll(params: Parameters): Set { - if (params.region != "") { - return imagesUsedByInstancesCache.get().elementAt(0).refdAmisByRegion[params.region] ?: emptySet() - } else { - throw IllegalArgumentException("Missing required region parameter") - } - } - - /** - * Returns an epochMs timestamp of the last cache update - */ - override fun getLastUpdated(): Long { - return imagesUsedByInstancesCache.get().elementAt(0).lastUpdated - } - - fun load(): Set { + override fun load(): AmazonImagesUsedByInstancesCache { val refdAmisByRegion = mutableMapOf>() val regions = workConfigurations.asSequence() @@ -81,24 +63,37 @@ open class EddaImagesUsedByInstancesProvider( refdAmisByRegion[region] = refdAmis } - return setOf( - AmazonImagesUsedByInstancesCache( - refdAmisByRegion, - clock.millis(), - "default" - ) - ) + return AmazonImagesUsedByInstancesCache(refdAmisByRegion, clock.millis(), "default") } } -@Configuration +@Component open class EddaImagesUsedByInstancesCache( eddaImagesUsedByInstancesProvider: EddaImagesUsedByInstancesProvider -) : InMemoryCache(eddaImagesUsedByInstancesProvider.load()) +) : InMemorySingletonCache(eddaImagesUsedByInstancesProvider.load()) data class AmazonImagesUsedByInstancesCache( - val refdAmisByRegion: Map>, - val lastUpdated: Long, + private val refdAmisByRegion: Map>, + private val lastUpdated: Long, override val name: String? -) : Cacheable +) : Cacheable { + private val log: Logger = LoggerFactory.getLogger(javaClass) + + /** + * @param params.region: return a Set of Amazon imageIds (i.e. "ami-abc123") + * currently referenced by running EC2 instances in the region + */ + fun getAll(params: Parameters): Set { + log.debug("getting all for ${javaClass.simpleName}") + if (params.region != "") { + return refdAmisByRegion[params.region] ?: emptySet() + } else { + throw IllegalArgumentException("Missing required region parameter") + } + } + + fun getLastUpdated(): Long { + return lastUpdated + } +} diff --git a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaLaunchConfigurationCacheProvider.kt b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaLaunchConfigurationCacheProvider.kt index 90793350..01806394 100644 --- a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaLaunchConfigurationCacheProvider.kt +++ b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/edda/providers/EddaLaunchConfigurationCacheProvider.kt @@ -20,50 +20,23 @@ import com.netflix.spinnaker.config.EddaApiClient import com.netflix.spinnaker.swabbie.* import com.netflix.spinnaker.swabbie.aws.launchconfigurations.AmazonLaunchConfiguration import com.netflix.spinnaker.swabbie.model.WorkConfiguration -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Lazy +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component import java.lang.IllegalArgumentException import java.time.Clock -@Configuration +@Component open class EddaLaunchConfigurationCacheProvider( private val clock: Clock, private val workConfigurations: List, private val launchConfigurationProvider: ResourceProvider, - private val eddaApiClients: List, - @Lazy private val launchConfigCache: InMemoryCache + private val eddaApiClients: List ) : CachedViewProvider { - /** - * @param params["region"]: return a Set across all known accounts in region - */ - override fun getAll(params: Parameters): Set { - if (params.region != "") { - return launchConfigCache.get().elementAt(0).configsByRegion[params.region] ?: emptySet() - } else { - throw IllegalArgumentException("Missing required region parameter") - } - } - - /** - * Returns an epochMs timestamp of the last cache update - */ - override fun getLastUpdated(): Long { - return launchConfigCache.get().elementAt(0).lastUpdated - } + private val log: Logger = LoggerFactory.getLogger(javaClass) - /** - * @param region: AWS region - * - * Returns a map of - */ - fun getRefdAmisForRegion(region: String): Map> { - val cache = launchConfigCache.get() - return cache.elementAt(0).refdAmisByRegion[region].orEmpty() - } - - // TODO("Refactor Cacheable to support singletons instead of just sets") - fun load(): Set { + override fun load(): AmazonLaunchConfigurationCache { val configsByRegion = mutableMapOf>() val refdAmisByRegion = mutableMapOf>>() @@ -94,26 +67,55 @@ open class EddaLaunchConfigurationCacheProvider( configsByRegion[region] = launchConfigs refdAmisByRegion[region] = refdAmis } + return AmazonLaunchConfigurationCache(configsByRegion, refdAmisByRegion, clock.millis(), "default") - return setOf( - AmazonLaunchConfigurationCache( - configsByRegion, - refdAmisByRegion, - clock.millis(), - "default" - ) - ) } } -@Configuration +@Component open class EddaLaunchConfigurationCache( eddaLaunchConfigurationCacheProvider: EddaLaunchConfigurationCacheProvider -) : InMemoryCache(eddaLaunchConfigurationCacheProvider.load()) +) : InMemorySingletonCache(eddaLaunchConfigurationCacheProvider.load()) data class AmazonLaunchConfigurationCache( - val configsByRegion: Map>, - val refdAmisByRegion: Map>>, - val lastUpdated: Long, + private val configsByRegion: Map>, + private val refdAmisByRegion: Map>>, + private val lastUpdated: Long, override val name: String? -) : Cacheable +) : Cacheable { + private val log: Logger = LoggerFactory.getLogger(javaClass) + + /** + * @param params["region"]: return a Set across all known accounts in region + */ + fun getLaunchConfigsByRegion(params: Parameters): Set { + if (params.region != "") { + return configsByRegion[params.region] ?: emptySet() + } else { + throw IllegalArgumentException("Missing required region parameter") + } + } + + fun getLaunchConfigsByRegionForImage(params: Parameters): Set { + log.debug("getting launch configs in ${javaClass.simpleName}") + if (params.region != "" && params.id != "") { + return getRefdAmisForRegion(params.region).getOrDefault(params.id, emptySet()) + } else { + throw IllegalArgumentException("Missing required region and id parameters") + } + } + + /** + * @param region: AWS region + * + * Returns a map of + */ + fun getRefdAmisForRegion(region: String): Map> { + log.debug("getting refd amis in ${javaClass.simpleName}") + return refdAmisByRegion[region].orEmpty() + } + + fun getLastUpdated(): Long { + return lastUpdated + } +} diff --git a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandler.kt b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandler.kt index 22ba03e2..3456e881 100644 --- a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandler.kt +++ b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandler.kt @@ -21,9 +21,11 @@ import com.netflix.spinnaker.config.SwabbieProperties import com.netflix.spinnaker.kork.core.RetrySupport import com.netflix.spinnaker.moniker.frigga.FriggaReflectiveNamer import com.netflix.spinnaker.swabbie.* -import com.netflix.spinnaker.swabbie.aws.instances.AmazonInstance -import com.netflix.spinnaker.swabbie.aws.launchconfigurations.AmazonLaunchConfiguration +import com.netflix.spinnaker.swabbie.aws.edda.providers.AmazonImagesUsedByInstancesCache +import com.netflix.spinnaker.swabbie.aws.edda.providers.AmazonLaunchConfigurationCache import com.netflix.spinnaker.swabbie.events.Action +import com.netflix.spinnaker.swabbie.exception.CacheSizeException +import com.netflix.spinnaker.swabbie.exception.StaleCacheException import com.netflix.spinnaker.swabbie.exclusions.ResourceExclusionPolicy import com.netflix.spinnaker.swabbie.model.* import com.netflix.spinnaker.swabbie.notifications.Notifier @@ -51,11 +53,10 @@ class AmazonImageHandler( applicationEventPublisher: ApplicationEventPublisher, lockingService: Optional, retrySupport: RetrySupport, + private val launchConfigurationCache: InMemorySingletonCache, + private val imagesUsedByinstancesCache: InMemorySingletonCache, private val rules: List>, private val imageProvider: ResourceProvider, - private val instanceProvider: ResourceProvider, - private val launchConfigurationProvider: ResourceProvider, - private val accountProvider: AccountProvider, private val orcaService: OrcaService, private val applicationsCaches: List>, private val taggingService: TaggingService, @@ -209,7 +210,7 @@ class AmazonImageHandler( * Checks references for: * 1. Instances. * 2. Launch Configurations. - * 3. Image siblings (Image sharing the same name) in other accounts/regions. + * 3. Seen in use recently. * Bubbles up any raised exception. */ private fun checkReferences(images: List?, params: Parameters) { @@ -227,9 +228,8 @@ class AmazonImageHandler( val elapsedTimeMillis = measureTimeMillis { try { - setUsedByInstances(images, params) setUsedByLaunchConfigurations(images, params) - setHasSiblings(images, params) + setUsedByInstances(images, params) setSeenWithinUnusedThreshold(images) } catch (e: Exception) { log.error("Failed to check image references. Params: {}", params, e) @@ -241,119 +241,74 @@ class AmazonImageHandler( } /** - * Checks if images are used by instances - */ - private fun setUsedByInstances( - images: List, - params: Parameters - ) { - instanceProvider.getAll(params).let { instances -> - log.info("Checking for references in {} instances", instances?.size) - if (instances == null || instances.isEmpty()) { - return - } - - images.filter { - NAIVE_EXCLUSION !in it.details && USED_BY_INSTANCES !in it.details - }.forEach { image -> - onMatchedImages( - instances.map { amazonInstance -> - SlimAwsResource(amazonInstance.name, amazonInstance.imageId, amazonInstance.getAutoscalingGroup().orEmpty()) - }, - image - ) { usedByResource -> - image.set(USED_BY_INSTANCES, true) - resourceUseTrackingRepository.recordUse(image.resourceId, usedByResource) - log.debug("Image {} ({}) in {} is USED_BY_INSTANCES", image.imageId, image.name, params.region) - } - } - } - } - - /** - * Checks if images are used by launch configurations + * Checks if images are used by launch configurations in all accounts */ private fun setUsedByLaunchConfigurations( images: List, params: Parameters ) { - launchConfigurationProvider.getAll(params).let { launchConfigurations -> - log.info("Checking for references in {} launch configurations", launchConfigurations?.size) - if (launchConfigurations == null || launchConfigurations.isEmpty()) { - log.debug("No launch configs found for params: {}", params) - return - } + if (launchConfigurationCache.get().getLastUpdated() + < clock.instant().toEpochMilli().minus(Duration.ofHours(1).toMillis())) { + throw StaleCacheException("Amazon launch configuration cache over 1 hour old, aborting.") + } + val imagesUsedByLaunchConfigsForRegion = launchConfigurationCache.get().getRefdAmisForRegion(params.region).keys + log.info("Checking the {} images used by launch configurations.", imagesUsedByLaunchConfigsForRegion.size) + if (imagesUsedByLaunchConfigsForRegion.size < swabbieProperties.minImagesUsedByLC) { + throw CacheSizeException("Amazon launch configuration cache contains less than " + + "${swabbieProperties.minImagesUsedByLC} images used, aborting for safety.") + } - images.filter { - NAIVE_EXCLUSION !in it.details && - USED_BY_INSTANCES !in it.details && - USED_BY_LAUNCH_CONFIGURATIONS !in it.details - }.forEach { image -> - onMatchedImages( - launchConfigurations.map { launchConfiguration -> - SlimAwsResource( - launchConfiguration.resourceId, - launchConfiguration.imageId, - launchConfiguration.getAutoscalingGroupName() - ) - }, - image - ) { usedByResource -> - log.debug("Image {} ({}) in {} is USED_BY_LAUNCH_CONFIGURATIONS", image.imageId, image.name, params.region) - resourceUseTrackingRepository.recordUse(image.resourceId, usedByResource) + images + .filter { NAIVE_EXCLUSION !in it.details } + .forEach { image -> + if (imagesUsedByLaunchConfigsForRegion.contains(image.imageId)) { + log.debug("Image {} ({}) in {} is USED_BY_LAUNCH_CONFIGS", image.imageId, image.name, params.region) image.set(USED_BY_LAUNCH_CONFIGURATIONS, true) + + val usedByLaunchConfigs = launchConfigurationCache + .get().getLaunchConfigsByRegionForImage(params.copy(id = image.imageId)) + .joinToString(",") { it.getAutoscalingGroupName() } + + resourceUseTrackingRepository.recordUse( + image.resourceId, + usedByLaunchConfigs + ) } } - } + } /** - * Checks if images have siblings in other accounts + * Checks if images are used by instances */ - private fun setHasSiblings( + private fun setUsedByInstances( images: List, params: Parameters ) { - log.info("Checking for sibling images.") - val imagesInOtherAccounts = getImagesFromOtherAccounts(params) - - val otherImagesIdToAccount = imagesInOtherAccounts - .map { (image, account) -> - image.imageId to account.accountId - }.toMap() - - val otherImageDescrToImageId = imagesInOtherAccounts - .filter { (image, account) -> - image.description != null - }.map { (image, account) -> - image.description!! to image.imageId - }.toMap() - - val filteredImages = images.filter { - NAIVE_EXCLUSION !in it.details && - USED_BY_INSTANCES !in it.details && - USED_BY_LAUNCH_CONFIGURATIONS !in it.details && - HAS_SIBLINGS_IN_OTHER_ACCOUNTS !in it.details && - IS_BASE_OR_ANCESTOR !in it.details + if (imagesUsedByinstancesCache.get().getLastUpdated() + < clock.instant().toEpochMilli().minus(Duration.ofHours(1).toMillis())) { + throw StaleCacheException("Amazon images used by instances cache over 1 hour old, aborting.") + } + val imagesUsedByInstancesInRegion = imagesUsedByinstancesCache.get().getAll(params) + log.info("Checking {} images used by instances", imagesUsedByInstancesInRegion .size) + if (imagesUsedByInstancesInRegion.size < swabbieProperties.minImagesUsedByInst) { + throw CacheSizeException("Amazon images used by instances cache contains less than " + + "${swabbieProperties.minImagesUsedByInst} images, aborting for safety.") } - val imageDescrToImageId = filteredImages - .filter { it.description != null } - .map { it.description!! to it.imageId } - .toMap() - - filteredImages.forEach { image -> - if (isAncestor(imageDescrToImageId, image) || isAncestor(otherImageDescrToImageId, image)) { - image.set(IS_BASE_OR_ANCESTOR, true) - image.set(NAIVE_EXCLUSION, true) - log.debug("Image {} ({}) in {} is IS_BASE_OR_ANCESTOR", image.imageId, image.name, params.region) - } + images + .filter { NAIVE_EXCLUSION !in it.details && USED_BY_LAUNCH_CONFIGURATIONS !in it.details } + .forEach { image -> + if (imagesUsedByInstancesInRegion.contains(image.imageId)) { + log.debug("Image {} ({}) in {} is USED_BY_INSTANCES", image.imageId, image.name, params.region) + image.set(USED_BY_INSTANCES, true) - if (otherImagesIdToAccount.containsKey(image.imageId)) { - image.set(HAS_SIBLINGS_IN_OTHER_ACCOUNTS, true) - log.debug("Image {} ({}) in {} is HAS_SIBLINGS_IN_OTHER_ACCOUNTS", image.imageId, image.name, params.region) + resourceUseTrackingRepository.recordUse( + image.resourceId, + "Used by an instance." + ) + } } - } } /** @@ -376,7 +331,6 @@ class AmazonImageHandler( NAIVE_EXCLUSION !in it.details && USED_BY_INSTANCES !in it.details && USED_BY_LAUNCH_CONFIGURATIONS !in it.details && - HAS_SIBLINGS_IN_OTHER_ACCOUNTS !in it.details && IS_BASE_OR_ANCESTOR !in it.details }.forEach { image -> if (!unusedAndTracked.containsKey(image.imageId) && usedImages.contains(image.imageId)) { @@ -387,37 +341,6 @@ class AmazonImageHandler( } } - /** - * Get images in accounts. Used to check for siblings in those accounts. - */ - private fun getImagesFromOtherAccounts( - params: Parameters - ): List> { - val result: MutableList> = mutableListOf() - accountProvider.getAccounts().filter { - it.type == AWS && it.accountId != params.account - }.forEach { account -> - account.regions?.forEach { region -> - log.info("Looking for other images in {}/{}", account.accountId, region) - imageProvider.getAll( - Parameters(account = account.accountId!!, region = region.name, environment = account.environment) - )?.forEach { image -> - result.add(Pair(image, account)) - } - } - } - - return result - } - - private fun onMatchedImages(ids: List, image: AmazonImage, onFound: (String) -> Unit) { - ids.forEach { - if (image.imageId == it.imageId) { - onFound.invoke(it.usedByResourceName) - } - } - } - override fun getCandidate(resourceId: String, resourceName: String, workConfiguration: WorkConfiguration): AmazonImage? { val params = Parameters( id = resourceId, @@ -438,9 +361,3 @@ private fun isAncestor(images: Map, image: AmazonImage): Boolean private fun AmazonImage.matches(image: AmazonImage): Boolean { return name == image.name || imageId == image.imageId } - -private data class SlimAwsResource( - val id: String, - val imageId: String, - val usedByResourceName: String -) diff --git a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/instances/AmazonInstance.kt b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/instances/AmazonInstance.kt index cbfcbad2..b76a0abb 100644 --- a/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/instances/AmazonInstance.kt +++ b/swabbie-aws/src/main/kotlin/com/netflix/spinnaker/swabbie/aws/instances/AmazonInstance.kt @@ -26,7 +26,7 @@ import java.time.ZoneId @JsonTypeName("amazonInstance") data class AmazonInstance( - private val instanceId: String, + val instanceId: String, val imageId: String, val tags: List>, private val launchTime: Long, diff --git a/swabbie-aws/src/test/java/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandlerTest.kt b/swabbie-aws/src/test/java/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandlerTest.kt index 691570c2..99f6cc98 100644 --- a/swabbie-aws/src/test/java/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandlerTest.kt +++ b/swabbie-aws/src/test/java/com/netflix/spinnaker/swabbie/aws/images/AmazonImageHandlerTest.kt @@ -27,6 +27,8 @@ import com.netflix.spinnaker.config.ResourceTypeConfiguration import com.netflix.spinnaker.config.SwabbieProperties import com.netflix.spinnaker.kork.core.RetrySupport import com.netflix.spinnaker.swabbie.* +import com.netflix.spinnaker.swabbie.aws.edda.providers.AmazonImagesUsedByInstancesCache +import com.netflix.spinnaker.swabbie.aws.edda.providers.AmazonLaunchConfigurationCache import com.netflix.spinnaker.swabbie.aws.instances.AmazonInstance import com.netflix.spinnaker.swabbie.aws.launchconfigurations.AmazonLaunchConfiguration import com.netflix.spinnaker.swabbie.events.MarkResourceEvent @@ -65,7 +67,12 @@ object AmazonImageHandlerTest { private val taggingService = mock() private val taskTrackingRepository = mock() private val resourceUseTrackingRepository = mock() - private val swabbieProperties = SwabbieProperties() + private val swabbieProperties = SwabbieProperties().apply { + minImagesUsedByInst = 0 + minImagesUsedByLC = 0 + } + private val launchConfigurationCache = mock>() + private val imagesUsedByinstancesCache = mock>() private val subject = AmazonImageHandler( clock = clock, @@ -84,15 +91,14 @@ object AmazonImageHandlerTest { lockingService = lockingService, retrySupport = RetrySupport(), imageProvider = imageProvider, - instanceProvider = instanceProvider, - launchConfigurationProvider = launchConfigurationProvider, orcaService = orcaService, - accountProvider = accountProvider, applicationsCaches = listOf(applicationsCache), taggingService = taggingService, taskTrackingRepository = taskTrackingRepository, resourceUseTrackingRepository = resourceUseTrackingRepository, - swabbieProperties = swabbieProperties + swabbieProperties = swabbieProperties, + launchConfigurationCache = launchConfigurationCache, + imagesUsedByinstancesCache = imagesUsedByinstancesCache ) @BeforeEach @@ -166,6 +172,32 @@ object AmazonImageHandlerTest { clock.instant().minus(Duration.ofDays(32)).toEpochMilli() ) ) + + val defaultLaunchConfig = AmazonLaunchConfiguration( + name = "test-app-v001-111920", + launchConfigurationName = "test-app-v001-111920", + imageId = "ami-132", + createdTime = clock.instant().toEpochMilli().minus(Duration.ofDays(3).toMillis()) + ) + + whenever(imagesUsedByinstancesCache.get()) doReturn + AmazonImagesUsedByInstancesCache( + mapOf( + "us-east-1" to setOf("ami-132","ami-444") + ), + clock.instant().toEpochMilli(), + "default" + ) + whenever(launchConfigurationCache.get()) doReturn + AmazonLaunchConfigurationCache( + mapOf( + "us-east-1" to setOf(defaultLaunchConfig) + ), + mapOf( + "us-east-1" to mapOf("ami-132" to setOf(defaultLaunchConfig)) + ), + clock.instant().toEpochMilli(), "default" + ) } @AfterEach @@ -179,7 +211,9 @@ object AmazonImageHandlerTest { launchConfigurationProvider, applicationEventPublisher, resourceOwnerResolver, - taskTrackingRepository + taskTrackingRepository, + imagesUsedByinstancesCache, + launchConfigurationCache ) } @@ -199,9 +233,8 @@ object AmazonImageHandlerTest { @Test fun `should fail to get candidates if checking launch configuration references fails`() { val configuration = getWorkConfiguration() - whenever(launchConfigurationProvider.getAll( - Parameters(account = "1234", region = "us-east-1", environment = "test") - )) doThrow IllegalStateException("launch configs") + whenever(launchConfigurationCache.get()) doThrow + IllegalStateException("launch configs") Assertions.assertThrows(IllegalStateException::class.java) { subject.preProcessCandidates(subject.getCandidates(getWorkConfiguration()).orEmpty(), configuration) @@ -211,29 +244,8 @@ object AmazonImageHandlerTest { @Test fun `should fail to get candidates if checking instance references fails`() { val configuration = getWorkConfiguration() - whenever(instanceProvider.getAll( - Parameters(account = "1234", region = "us-east-1", environment = "test") - )) doThrow IllegalStateException("failed to get instances") - - Assertions.assertThrows(IllegalStateException::class.java) { - subject.preProcessCandidates(subject.getCandidates(getWorkConfiguration()).orEmpty(), configuration) - } - } - - @Test - fun `should fail to get candidates if checking for siblings fails because of accounts`() { - whenever(accountProvider.getAccounts()) doThrow IllegalStateException("failed to get accounts") - Assertions.assertThrows(IllegalStateException::class.java) { - subject.getCandidates(getWorkConfiguration()) - } - } - - @Test - fun `should fail to get candidates if checking for siblings in other accounts fails`() { - val configuration = getWorkConfiguration() - whenever(imageProvider.getAll( - Parameters(account = "1234", region = "us-east-1", environment = "test") - )) doThrow IllegalStateException("failed to get images in 4321/us-east-1") + whenever(imagesUsedByinstancesCache.get()) doThrow + IllegalStateException("failed to get instances") Assertions.assertThrows(IllegalStateException::class.java) { subject.preProcessCandidates(subject.getCandidates(getWorkConfiguration()).orEmpty(), configuration) @@ -279,6 +291,15 @@ object AmazonImageHandlerTest { @Test fun `should not mark images referenced by other resources`() { + whenever(imagesUsedByinstancesCache.get()) doReturn + AmazonImagesUsedByInstancesCache( + mapOf( + "us-east-1" to setOf("ami-123","ami-444") + ), + clock.instant().toEpochMilli(), + "default" + ) + val workConfiguration = getWorkConfiguration( exclusionList = mutableListOf( Exclusion() @@ -295,29 +316,13 @@ object AmazonImageHandlerTest { ) ) - whenever(instanceProvider.getAll(any())) doReturn - listOf( - AmazonInstance( - instanceId = "i-123", - cloudProvider = AWS, - imageId = "ami-123", // reference to ami-123 - launchTime = clock.instant().toEpochMilli(), - tags = listOf(mapOf( - "class" to "com.amazonaws.services.ec2.model.Tag", - "value" to "test-servergroup-v111", - "key" to "aws:autoscaling:groupName", - "aws:autoscaling:groupName" to "test-servergroup-v111" - )) - ) - ) - subject.getCandidates(workConfiguration).let { images -> images!!.size shouldMatch equalTo(2) Assertions.assertTrue(images.any { it.imageId == "ami-123" }) Assertions.assertTrue(images.any { it.imageId == "ami-132" }) } - subject.mark(workConfiguration, { print { "postMark" } }) + subject.mark(workConfiguration) { print { "postMark" } } // ami-132 is excluded by exclusion policies, specifically because ami-132 is not allowlisted // ami-123 is referenced by an instance, so therefore should not be marked for deletion @@ -329,8 +334,6 @@ object AmazonImageHandlerTest { fun `should not mark ancestor or base images`() { val workConfiguration = getWorkConfiguration() val params = Parameters(account = "1234", region = "us-east-1", environment = "test") - whenever(instanceProvider.getAll(any())) doReturn listOf() - whenever(launchConfigurationProvider.getAll(any())) doReturn listOf() whenever(imageProvider.getAll(params)) doReturn listOf( AmazonImage( imageId = "ami-123", @@ -411,7 +414,7 @@ object AmazonImageHandlerTest { whenever(imageProvider.getAll(params)) doReturn listOf(image) whenever(orcaService.orchestrate(any())) doReturn TaskResponse(ref = "/tasks/1234") - subject.delete(workConfiguration, {}) + subject.delete(workConfiguration) {} verify(taskTrackingRepository, times(1)).add(any(), any()) verify(orcaService, times(1)).orchestrate(any()) diff --git a/swabbie-clouddriver/src/main/kotlin/com/netflix/spinnaker/swabbie/clouddriver/ClouddriverAccountProvider.kt b/swabbie-clouddriver/src/main/kotlin/com/netflix/spinnaker/swabbie/clouddriver/ClouddriverAccountProvider.kt index b857eb4f..540470fe 100644 --- a/swabbie-clouddriver/src/main/kotlin/com/netflix/spinnaker/swabbie/clouddriver/ClouddriverAccountProvider.kt +++ b/swabbie-clouddriver/src/main/kotlin/com/netflix/spinnaker/swabbie/clouddriver/ClouddriverAccountProvider.kt @@ -20,7 +20,6 @@ import com.netflix.spinnaker.swabbie.AccountProvider import com.netflix.spinnaker.swabbie.InMemoryCache import com.netflix.spinnaker.swabbie.model.Account import com.netflix.spinnaker.swabbie.model.SpinnakerAccount -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression import org.springframework.stereotype.Component @Component diff --git a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/config/SwabbieProperties.kt b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/config/SwabbieProperties.kt index 266c1ee1..30dbffbc 100644 --- a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/config/SwabbieProperties.kt +++ b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/config/SwabbieProperties.kt @@ -23,6 +23,12 @@ import java.time.DayOfWeek import java.time.LocalTime import java.time.ZoneId +/** + * @param minImagesUsedByLC minimum number of images used by launch configurations that should be + * in the cache. If not present agents will error to prevent mass marking in the case of edda problems. + * @param minImagesUsedByInst minimum number of images used by instances that should be + * in the cache. If not present agents will error to prevent mass marking in the case of edda problems. + */ @ConfigurationProperties("swabbie") open class SwabbieProperties { var dryRun: Boolean = true @@ -30,6 +36,8 @@ open class SwabbieProperties { var providers: List = mutableListOf() var schedule: Schedule = Schedule() var testing: Testing = Testing() + var minImagesUsedByLC = 500 + var minImagesUsedByInst = 500 } class Testing { diff --git a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/Cacheable.kt b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/Cacheable.kt index 37d46e41..e07262e3 100644 --- a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/Cacheable.kt +++ b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/Cacheable.kt @@ -29,6 +29,10 @@ interface Cache { fun contains(key: String?): Boolean } +interface SingletonCache { + fun get(): T +} + open class InMemoryCache( private val source: Set ) : Cache { @@ -58,3 +62,27 @@ open class InMemoryCache( val log: Logger = LoggerFactory.getLogger(javaClass) } + +open class InMemorySingletonCache( + private val source: T +) : SingletonCache { + val log: Logger = LoggerFactory.getLogger(javaClass) + + private val cache = AtomicReference() + + @Scheduled(fixedDelay = 15 * 60 * 1000L) //TODO: make configurable + private fun refresh() { + try { + cache.set(source) + } catch (e: Exception) { + log.error("Error refreshing cache ${javaClass.name}", e) + } + } + + override fun get(): T { + if (cache.get() == null) { + cache.set(source) + } + return cache.get() + } +} diff --git a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/CachedViewProvider.kt b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/CachedViewProvider.kt index 92f22c8b..3ef4326d 100644 --- a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/CachedViewProvider.kt +++ b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/CachedViewProvider.kt @@ -1,10 +1,5 @@ package com.netflix.spinnaker.swabbie interface CachedViewProvider { - /** - * Gets all resources with [Parameters] - */ - fun getAll(params: Parameters): Collection - - fun getLastUpdated(): Long + fun load(): T } diff --git a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/CacheSizeException.kt b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/CacheSizeException.kt new file mode 100644 index 00000000..3a649b4d --- /dev/null +++ b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/CacheSizeException.kt @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2018 Netflix, 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.netflix.spinnaker.swabbie.exception + +class CacheSizeException: RuntimeException { + constructor(message: String) : super(message) + constructor(message: String, cause: Throwable) : super(message, cause) +} diff --git a/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/StaleCacheException.kt b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/StaleCacheException.kt new file mode 100644 index 00000000..841b7e84 --- /dev/null +++ b/swabbie-core/src/main/kotlin/com/netflix/spinnaker/swabbie/exception/StaleCacheException.kt @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2018 Netflix, 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.netflix.spinnaker.swabbie.exception + +class StaleCacheException: RuntimeException { + constructor(message: String) : super(message) + constructor(message: String, cause: Throwable) : super(message, cause) +}