com.netflix.spinnaker.keel.clouddriver.MemoryCloudDriverCache.kt Maven / Gradle / Ivy
The newest version!
/*
* 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.keel.clouddriver
import com.github.benmanes.caffeine.cache.AsyncCache
import com.github.benmanes.caffeine.cache.AsyncLoadingCache
import com.netflix.spinnaker.keel.caffeine.BulkCacheLoadingException
import com.netflix.spinnaker.keel.caffeine.CacheFactory
import com.netflix.spinnaker.keel.caffeine.CacheLoadingException
import com.netflix.spinnaker.keel.clouddriver.model.Certificate
import com.netflix.spinnaker.keel.clouddriver.model.Credential
import com.netflix.spinnaker.keel.clouddriver.model.Network
import com.netflix.spinnaker.keel.clouddriver.model.SecurityGroupSummary
import com.netflix.spinnaker.keel.clouddriver.model.Subnet
import com.netflix.spinnaker.keel.core.api.DEFAULT_SERVICE_ACCOUNT
import com.netflix.spinnaker.keel.retrofit.isNotFound
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import retrofit2.HttpException
import java.time.Duration
import java.util.concurrent.CompletableFuture.completedFuture
/**
* An in-memory cache for calls against cloud driver
*
* Caching is implemented using asynchronous caches ([AsyncCache]) because
* it isn't safe for a kotlin coroutine to yield inside of the second argument
* of a synchronous cache's get method.
*
* For more details on why using async, see: https://github.com/spinnaker/keel/pull/1154
*/
class MemoryCloudDriverCache(
private val cloudDriver: CloudDriverService,
cacheFactory: CacheFactory
) : CloudDriverCache {
private val securityGroupsById: AsyncLoadingCache, SecurityGroupSummary> = cacheFactory
.asyncLoadingCache(
cacheName = "securityGroupsById",
defaultExpireAfterWrite = Duration.ofMinutes(10)
) { key ->
val (account, region, id) = key
runCatching {
val credential = credentialBy(account)
cloudDriver.getSecurityGroupSummaryById(account, credential.type, region, id, DEFAULT_SERVICE_ACCOUNT)
.also {
securityGroupsByName.put(Triple(account, region, it.name), completedFuture(it))
}
}
.handleNotFound("securityGroupsById", key)
}
private val securityGroupsByName: AsyncLoadingCache, SecurityGroupSummary> = cacheFactory
.asyncLoadingCache(
cacheName = "securityGroupsByName",
defaultExpireAfterWrite = Duration.ofMinutes(10)
) { key ->
val (account, region, name) = key
runCatching {
val credential = credentialBy(account)
cloudDriver.getSecurityGroupSummaryByName(account, credential.type, region, name, DEFAULT_SERVICE_ACCOUNT)
.also {
securityGroupsById.put(Triple(account, region, it.id), completedFuture(it))
}
}
.handleNotFound("securityGroupsByName", key)
}
private val networksById: AsyncLoadingCache = cacheFactory
.asyncBulkLoadingCache(cacheName = "networksById") {
runCatching {
cloudDriver.listNetworks("aws", DEFAULT_SERVICE_ACCOUNT)
.associateBy { it.id }
}
.getOrElse { ex ->
throw BulkCacheLoadingException("networksById", ex)
}
}
private val networksByName: AsyncLoadingCache, Network> = cacheFactory
.asyncBulkLoadingCache(cacheName = "networksByName") {
runCatching {
cloudDriver
.listNetworks("aws", DEFAULT_SERVICE_ACCOUNT)
.associateBy {
Triple(it.account, it.region, it.name)
}
}
.getOrElse { ex ->
throw BulkCacheLoadingException("networksByName", ex)
}
}
private data class AvailabilityZoneKey(
val account: String,
val region: String,
val vpcId: String,
val purpose: String
)
private val availabilityZones: AsyncLoadingCache> = cacheFactory
.asyncLoadingCache(
cacheName = "availabilityZones"
) { key ->
val (account, region, vpcId, purpose) = key
runCatching {
cloudDriver
.listSubnets("aws", DEFAULT_SERVICE_ACCOUNT)
.filter { it.account == account && it.vpcId == vpcId && it.purpose == purpose && it.region == region }
.map { it.availabilityZone }
.toSet()
}
.getOrElse { ex ->
throw CacheLoadingException("availabilityZones", key, ex)
}
}
private val credentials: AsyncLoadingCache = cacheFactory
.asyncLoadingCache(
cacheName = "credentials"
) { name ->
runCatching {
cloudDriver.getCredential(name, DEFAULT_SERVICE_ACCOUNT)
}
.handleNotFound("credentials", name)
}
private val subnetsById: AsyncLoadingCache = cacheFactory
.asyncBulkLoadingCache(cacheName = "subnetsById") {
runCatching {
cloudDriver
.listSubnets("aws", DEFAULT_SERVICE_ACCOUNT)
.associateBy { it.id }
}
.getOrElse { ex -> throw BulkCacheLoadingException("subnetsById", ex) }
}
private val subnetsByPurpose: AsyncLoadingCache, Subnet> = cacheFactory
.asyncBulkLoadingCache(cacheName = "subnetsByPurpose") {
runCatching {
cloudDriver
.listSubnets("aws", DEFAULT_SERVICE_ACCOUNT)
.associateBy { Triple(it.account, it.region, it.purpose) }
}
.getOrElse { ex -> throw BulkCacheLoadingException("subnetsByPurpose", ex) }
}
private data class CertificateKey(val account: String, val name: String) {
constructor(certificate: Certificate) : this(
certificate.accountName,
certificate.serverCertificateName
)
}
private val certificatesByAccountAndName: AsyncLoadingCache =
cacheFactory
.asyncBulkLoadingCache("certificatesByAccountAndName") {
runCatching {
cloudDriver
.getCertificates()
.associateBy { CertificateKey(it) }
}
.getOrElse { ex -> throw BulkCacheLoadingException("certificatesByName", ex) }
}
private val certificatesByArn: AsyncLoadingCache =
cacheFactory
.asyncBulkLoadingCache("certificatesByArn") {
runCatching {
cloudDriver
.getCertificates()
.associateBy { it.arn }
}
.getOrElse { ex -> throw BulkCacheLoadingException("certificatesByArn", ex) }
}
override fun credentialBy(name: String): Credential =
runBlocking {
credentials.get(name).await() ?: notFound("Credential with name $name not found")
}
override fun securityGroupById(account: String, region: String, id: String): SecurityGroupSummary =
runBlocking {
securityGroupsById.get(Triple(account, region, id)).await()?: notFound("Security group with id $id not found in $account:$region")
}
override fun securityGroupByName(account: String, region: String, name: String): SecurityGroupSummary =
runBlocking {
securityGroupsByName.get(Triple(account, region, name)).await()?: notFound("Security group with name $name not found in $account:$region")
}
override fun networkBy(id: String): Network =
runBlocking {
networksById.get(id).await() ?: notFound("VPC network with id $id not found")
}
override fun networkBy(name: String?, account: String, region: String): Network =
runBlocking {
networksByName.get(Triple(account, region, name)).await() ?: notFound("VPC network named $name not found in $account:$region")
}
override fun availabilityZonesBy(account: String, vpcId: String, purpose: String, region: String): Set =
runBlocking {
availabilityZones.get(AvailabilityZoneKey(account, region, vpcId, purpose)).await() ?: notFound("Availability zone with purpose \"$purpose\" not found in $account:$region")
}
override fun subnetBy(subnetId: String): Subnet =
runBlocking {
subnetsById.get(subnetId).await() ?: notFound("Subnet with id $subnetId not found")
}
override fun subnetBy(account: String, region: String, purpose: String): Subnet =
runBlocking {
subnetsByPurpose.get(Triple(account, region, purpose)).await() ?: notFound("Subnet with purpose \"$purpose\" not found in $account:$region")
}
override fun certificateByAccountAndName(account: String, name: String): Certificate =
runBlocking {
certificatesByAccountAndName.get(CertificateKey(account, name)).await() ?: notFound("Certificate with name $name not found")
}
override fun certificateByArn(arn: String): Certificate =
runBlocking {
certificatesByArn.get(arn).await() ?: notFound("Certificate with ARN $arn not found")
}
}
/**
* Translates a 404 from a Retrofit [HttpException] into a `null`. Any other exception is wrapped in
* [CacheLoadingException].
*/
private fun Result.handleNotFound(cacheName: String, key: Any): V? =
getOrElse { ex ->
if (ex.isNotFound) {
null
} else {
throw CacheLoadingException(cacheName, key, ex)
}
}
private fun notFound(message: String): Nothing = throw ResourceNotFound(message)