Skip to content

Commit 8824af8

Browse files
committed
Merge branch 'master' of github.com:spinnaker/keel
2 parents d2bbd7e + 657fba2 commit 8824af8

File tree

30 files changed

+507
-152
lines changed

30 files changed

+507
-152
lines changed

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ subprojects {
6262

6363
"testImplementation"("org.junit.platform:junit-platform-runner")
6464
"testImplementation"("org.junit.jupiter:junit-jupiter-api")
65+
"testImplementation"("org.junit.jupiter:junit-jupiter-params")
6566
"testImplementation"("io.mockk:mockk")
6667
"testImplementation"("org.jacoco:org.jacoco.ant:0.8.5")
6768

keel-clouddriver/src/main/kotlin/com/netflix/spinnaker/keel/clouddriver/CloudDriverCache.kt

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.netflix.spinnaker.keel.clouddriver
1717

18+
import com.netflix.spinnaker.keel.clouddriver.model.Certificate
1819
import com.netflix.spinnaker.keel.clouddriver.model.Credential
1920
import com.netflix.spinnaker.keel.clouddriver.model.Network
2021
import com.netflix.spinnaker.keel.clouddriver.model.SecurityGroupSummary
@@ -32,6 +33,8 @@ interface CloudDriverCache {
3233
fun credentialBy(name: String): Credential
3334
fun defaultKeyPairForAccount(account: String) =
3435
credentialBy(account).attributes["defaultKeyPair"] as String
36+
fun certificateByName(name: String): Certificate
37+
fun certificateByArn(arn: String): Certificate
3538
}
3639

3740
class ResourceNotFound(message: String) : IntegrationException(message)

keel-clouddriver/src/main/kotlin/com/netflix/spinnaker/keel/clouddriver/CloudDriverService.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.netflix.spinnaker.keel.clouddriver
1818
import com.netflix.spinnaker.keel.clouddriver.model.ActiveServerGroup
1919
import com.netflix.spinnaker.keel.clouddriver.model.AmazonLoadBalancer
2020
import com.netflix.spinnaker.keel.clouddriver.model.ApplicationLoadBalancerModel
21+
import com.netflix.spinnaker.keel.clouddriver.model.Certificate
2122
import com.netflix.spinnaker.keel.clouddriver.model.ClassicLoadBalancerModel
2223
import com.netflix.spinnaker.keel.clouddriver.model.Credential
2324
import com.netflix.spinnaker.keel.clouddriver.model.DockerImage
@@ -66,10 +67,11 @@ interface CloudDriverService {
6667
@Header("X-SPINNAKER-USER") user: String = DEFAULT_SERVICE_ACCOUNT
6768
): SecurityGroupSummary
6869

69-
@GET("/networks")
70+
@GET("/networks/{cloudProvider}")
7071
suspend fun listNetworks(
72+
@Path("cloudProvider") cloudProvider: String,
7173
@Header("X-SPINNAKER-USER") user: String = DEFAULT_SERVICE_ACCOUNT
72-
): Map<String, Set<Network>>
74+
): Set<Network>
7375

7476
@GET("/subnets/{cloudProvider}")
7577
suspend fun listSubnets(
@@ -196,4 +198,7 @@ interface CloudDriverService {
196198
@Query("entityId") entityId: String,
197199
@Header("X-SPINNAKER-USER") user: String = DEFAULT_SERVICE_ACCOUNT
198200
): List<EntityTags>
201+
202+
@GET("/certificates/aws")
203+
suspend fun getCertificates() : List<Certificate>
199204
}

keel-clouddriver/src/main/kotlin/com/netflix/spinnaker/keel/clouddriver/MemoryCloudDriverCache.kt

+62-36
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.netflix.spinnaker.keel.clouddriver
1818
import com.github.benmanes.caffeine.cache.AsyncCache
1919
import com.github.benmanes.caffeine.cache.AsyncLoadingCache
2020
import com.netflix.spinnaker.keel.caffeine.CacheFactory
21+
import com.netflix.spinnaker.keel.clouddriver.model.Certificate
2122
import com.netflix.spinnaker.keel.clouddriver.model.Credential
2223
import com.netflix.spinnaker.keel.clouddriver.model.Network
2324
import com.netflix.spinnaker.keel.clouddriver.model.SecurityGroupSummary
@@ -56,7 +57,6 @@ class MemoryCloudDriverCache(
5657
}
5758
}
5859
.handleNotFound()
59-
?: notFound("Security group with id $id not found in the $account account and $region region")
6060
}
6161

6262
private val securityGroupsByName: AsyncLoadingCache<Triple<String, String, String>, SecurityGroupSummary> = cacheFactory
@@ -72,34 +72,31 @@ class MemoryCloudDriverCache(
7272
}
7373
}
7474
.handleNotFound()
75-
?: notFound("Security group with name $name not found in the $account account and $region region")
7675
}
7776

7877
private val networksById: AsyncLoadingCache<String, Network> = cacheFactory
79-
.asyncLoadingCache(cacheName = "networksById") { id ->
78+
.asyncBulkLoadingCache(cacheName = "networksById") {
8079
runCatching {
81-
cloudDriver.listNetworks(DEFAULT_SERVICE_ACCOUNT)["aws"]
82-
?.firstOrNull { it.id == id }
83-
?.also {
84-
networksByName.put(Triple(it.account, it.region, it.name), completedFuture(it))
85-
}
80+
cloudDriver.listNetworks("aws", DEFAULT_SERVICE_ACCOUNT)
81+
.associateBy { it.id }
8682
}
87-
.handleNotFound()
88-
?: notFound("VPC network with id $id not found")
83+
.getOrElse { ex ->
84+
throw CacheLoadingException("Error loading networksById cache", ex)
85+
}
8986
}
9087

9188
private val networksByName: AsyncLoadingCache<Triple<String, String, String?>, Network> = cacheFactory
92-
.asyncLoadingCache(cacheName = "networksByName") { (account, region, name) ->
89+
.asyncBulkLoadingCache(cacheName = "networksByName") {
9390
runCatching {
9491
cloudDriver
95-
.listNetworks(DEFAULT_SERVICE_ACCOUNT)["aws"]
96-
?.firstOrNull { it.name == name && it.account == account && it.region == region }
97-
?.also {
98-
networksById.put(it.id, completedFuture(it))
92+
.listNetworks("aws", DEFAULT_SERVICE_ACCOUNT)
93+
.associateBy {
94+
Triple(it.account, it.region, it.name)
9995
}
10096
}
101-
.handleNotFound()
102-
?: notFound("VPC network named $name not found in $region")
97+
.getOrElse { ex ->
98+
throw CacheLoadingException("Error loading networksByName cache", ex)
99+
}
103100
}
104101

105102
private data class AvailabilityZoneKey(
@@ -121,7 +118,7 @@ class MemoryCloudDriverCache(
121118
.toSet()
122119
}
123120
.getOrElse { ex ->
124-
throw CacheLoadingException("Error loading cache", ex)
121+
throw CacheLoadingException("Error loading availabilityZones cache", ex)
125122
}
126123
}
127124

@@ -133,69 +130,98 @@ class MemoryCloudDriverCache(
133130
cloudDriver.getCredential(name, DEFAULT_SERVICE_ACCOUNT)
134131
}
135132
.handleNotFound()
136-
?: notFound("Credentials with name $name not found")
137133
}
138134

139135
private val subnetsById: AsyncLoadingCache<String, Subnet> = cacheFactory
140-
.asyncLoadingCache(cacheName = "subnetsById") { subnetId ->
136+
.asyncBulkLoadingCache(cacheName = "subnetsById") {
141137
runCatching {
142138
cloudDriver
143139
.listSubnets("aws", DEFAULT_SERVICE_ACCOUNT)
144-
.find { it.id == subnetId }
140+
.associateBy { it.id }
145141
}
146-
.handleNotFound()
147-
?: notFound("Subnet with id $subnetId not found")
142+
.getOrElse { ex -> throw CacheLoadingException("Error loading subnetsById cache", ex) }
148143
}
149144

150-
private val subnetsByPurpose: AsyncLoadingCache<Triple<String, String, String>, Subnet> = cacheFactory
151-
.asyncLoadingCache(cacheName = "subnetsByPurpose") { (account, region, purpose) ->
145+
private val subnetsByPurpose: AsyncLoadingCache<Triple<String, String, String?>, Subnet> = cacheFactory
146+
.asyncBulkLoadingCache(cacheName = "subnetsByPurpose") {
152147
runCatching {
153148
cloudDriver
154149
.listSubnets("aws", DEFAULT_SERVICE_ACCOUNT)
155-
.find { it.account == account && it.region == region && it.purpose == purpose }
150+
.associateBy { Triple(it.account, it.region, it.purpose) }
156151
}
157-
.handleNotFound()
158-
?: notFound("Subnet with purpose \"$purpose\" not found in $account:$region")
152+
.getOrElse { ex -> throw CacheLoadingException("Error loading subnetsByPurpose cache", ex) }
159153
}
160154

155+
private val certificatesByName: AsyncLoadingCache<String, Certificate> =
156+
cacheFactory
157+
.asyncBulkLoadingCache("certificatesByName") {
158+
runCatching {
159+
cloudDriver
160+
.getCertificates()
161+
.associateBy { it.serverCertificateName }
162+
}
163+
.getOrElse { ex -> throw CacheLoadingException("Error loading certificatesByName cache", ex) }
164+
}
165+
166+
private val certificatesByArn: AsyncLoadingCache<String, Certificate> =
167+
cacheFactory
168+
.asyncBulkLoadingCache("certificatesByArn") {
169+
runCatching {
170+
cloudDriver
171+
.getCertificates()
172+
.associateBy { it.arn }
173+
}
174+
.getOrElse { ex -> throw CacheLoadingException("Error loading certificatesByArn cache", ex) }
175+
}
176+
161177
override fun credentialBy(name: String): Credential =
162178
runBlocking {
163-
credentials.get(name).await()
179+
credentials.get(name).await() ?: notFound("Credential with name $name not found")
164180
}
165181

166182
override fun securityGroupById(account: String, region: String, id: String): SecurityGroupSummary =
167183
runBlocking {
168-
securityGroupsById.get(Triple(account, region, id)).await()
184+
securityGroupsById.get(Triple(account, region, id)).await()?: notFound("Security group with id $id not found in $account:$region")
169185
}
170186

171187
override fun securityGroupByName(account: String, region: String, name: String): SecurityGroupSummary =
172188
runBlocking {
173-
securityGroupsByName.get(Triple(account, region, name)).await()
189+
securityGroupsByName.get(Triple(account, region, name)).await()?: notFound("Security group with name $name not found in $account:$region")
174190
}
175191

176192
override fun networkBy(id: String): Network =
177193
runBlocking {
178-
networksById.get(id).await()
194+
networksById.get(id).await() ?: notFound("VPC network with id $id not found")
179195
}
180196

181197
override fun networkBy(name: String?, account: String, region: String): Network =
182198
runBlocking {
183-
networksByName.get(Triple(account, region, name)).await()
199+
networksByName.get(Triple(account, region, name)).await() ?: notFound("VPC network named $name not found in $account:$region")
184200
}
185201

186202
override fun availabilityZonesBy(account: String, vpcId: String, purpose: String, region: String): Set<String> =
187203
runBlocking {
188-
availabilityZones.get(AvailabilityZoneKey(account, region, vpcId, purpose)).await()
204+
availabilityZones.get(AvailabilityZoneKey(account, region, vpcId, purpose)).await() ?: notFound("Availability zone with purpose \"$purpose\" not found in $account:$region")
189205
}
190206

191207
override fun subnetBy(subnetId: String): Subnet =
192208
runBlocking {
193-
subnetsById.get(subnetId).await()
209+
subnetsById.get(subnetId).await() ?: notFound("Subnet with id $subnetId not found")
194210
}
195211

196212
override fun subnetBy(account: String, region: String, purpose: String): Subnet =
197213
runBlocking {
198-
subnetsByPurpose.get(Triple(account, region, purpose)).await()
214+
subnetsByPurpose.get(Triple(account, region, purpose)).await() ?: notFound("Subnet with purpose \"$purpose\" not found in $account:$region")
215+
}
216+
217+
override fun certificateByName(name: String): Certificate =
218+
runBlocking {
219+
certificatesByName.get(name).await() ?: notFound("Certificate with name $name not found")
220+
}
221+
222+
override fun certificateByArn(arn: String): Certificate =
223+
runBlocking {
224+
certificatesByArn.get(arn).await() ?: notFound("Certificate with ARN $arn not found")
199225
}
200226
}
201227

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.netflix.spinnaker.keel.clouddriver.model
2+
3+
data class Certificate(val serverCertificateName: String, val arn: String)

keel-clouddriver/src/test/kotlin/com/netflix/spinnaker/keel/clouddriver/MemoryCloudDriverCacheTest.kt

+88-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.netflix.spinnaker.keel.clouddriver
22

33
import com.netflix.spinnaker.keel.caffeine.TEST_CACHE_FACTORY
4+
import com.netflix.spinnaker.keel.clouddriver.model.Certificate
45
import com.netflix.spinnaker.keel.clouddriver.model.Credential
56
import com.netflix.spinnaker.keel.clouddriver.model.Network
67
import com.netflix.spinnaker.keel.clouddriver.model.SecurityGroupSummary
@@ -9,6 +10,8 @@ import com.netflix.spinnaker.keel.retrofit.RETROFIT_NOT_FOUND
910
import com.netflix.spinnaker.keel.retrofit.RETROFIT_SERVICE_UNAVAILABLE
1011
import io.mockk.mockk
1112
import org.junit.jupiter.api.Test
13+
import org.junit.jupiter.params.ParameterizedTest
14+
import org.junit.jupiter.params.provider.ValueSource
1215
import strikt.api.expectThat
1316
import strikt.api.expectThrows
1417
import strikt.assertions.containsExactly
@@ -51,6 +54,11 @@ internal class MemoryCloudDriverCacheTest {
5154
Subnet("d", "vpc-3", "prod", "us-west-2", "us-west-2d", "external (vpc3)")
5255
)
5356

57+
val certificates = listOf(
58+
Certificate("cert-1", "arn:cert-1"),
59+
Certificate("cert-2", "arn:cert-2")
60+
)
61+
5462
@Test
5563
fun `security groups are looked up from CloudDriver when accessed by id`() {
5664
every {
@@ -133,8 +141,8 @@ internal class MemoryCloudDriverCacheTest {
133141
@Test
134142
fun `VPC networks are looked up by id from CloudDriver`() {
135143
every {
136-
cloudDriver.listNetworks()
137-
} returns mapOf("aws" to vpcs)
144+
cloudDriver.listNetworks("aws")
145+
} returns vpcs
138146

139147
subject.networkBy("vpc-2").let { vpc ->
140148
expectThat(vpc) {
@@ -148,8 +156,8 @@ internal class MemoryCloudDriverCacheTest {
148156
@Test
149157
fun `an invalid VPC id throws an exception`() {
150158
every {
151-
cloudDriver.listNetworks()
152-
} returns mapOf("aws" to vpcs)
159+
cloudDriver.listNetworks("aws")
160+
} returns vpcs
153161

154162
expectThrows<ResourceNotFound> {
155163
subject.networkBy("vpc-5")
@@ -159,8 +167,8 @@ internal class MemoryCloudDriverCacheTest {
159167
@Test
160168
fun `VPC networks are looked up by name and region from CloudDriver`() {
161169
every {
162-
cloudDriver.listNetworks()
163-
} returns mapOf("aws" to vpcs)
170+
cloudDriver.listNetworks("aws")
171+
} returns vpcs
164172

165173
subject.networkBy("vpcName", "test", "us-west-2").let { vpc ->
166174
expectThat(vpc.id).isEqualTo("vpc-2")
@@ -170,8 +178,8 @@ internal class MemoryCloudDriverCacheTest {
170178
@Test
171179
fun `an invalid VPC name and region throws an exception`() {
172180
every {
173-
cloudDriver.listNetworks()
174-
} returns mapOf("aws" to vpcs)
181+
cloudDriver.listNetworks("aws")
182+
} returns vpcs
175183

176184
expectThrows<ResourceNotFound> {
177185
subject.networkBy("invalid", "prod", "us-west-2")
@@ -211,4 +219,76 @@ internal class MemoryCloudDriverCacheTest {
211219
subject.availabilityZonesBy("prod", "vpc-3", "external (vpc3)", "us-west-2")
212220
).containsExactly("us-west-2d")
213221
}
222+
223+
@ParameterizedTest
224+
@ValueSource(strings = ["cert-1", "cert-2"])
225+
fun `certificates are looked up from CloudDriver when requested by name`(name: String) {
226+
every { cloudDriver.getCertificates() } returns certificates
227+
228+
expectThat(subject.certificateByName(name))
229+
.get { serverCertificateName } isEqualTo name
230+
}
231+
232+
@Test
233+
fun `an unknown certificate name throws an exception`() {
234+
every { cloudDriver.getCertificates() } returns certificates
235+
236+
expectThrows<ResourceNotFound> { subject.certificateByName("does-not-exist") }
237+
}
238+
239+
@ParameterizedTest
240+
@ValueSource(strings = ["cert-1", "cert-2"])
241+
fun `subsequent requests for a certificate by name hit the cache`(name: String) {
242+
every { cloudDriver.getCertificates() } returns certificates
243+
244+
repeat(4) { subject.certificateByName(name) }
245+
246+
verify(exactly = 1) { cloudDriver.getCertificates() }
247+
}
248+
249+
@Test
250+
fun `all certs are cached at once when requested by name`() {
251+
every { cloudDriver.getCertificates() } returns certificates
252+
253+
listOf("cert-1", "cert-2")
254+
.forEach(subject::certificateByName)
255+
256+
verify(exactly = 1) { cloudDriver.getCertificates() }
257+
}
258+
259+
@ParameterizedTest
260+
@ValueSource(strings = ["arn:cert-1", "arn:cert-2"])
261+
fun `certificates are looked up from CloudDriver when requested by ARN`(arn: String) {
262+
every { cloudDriver.getCertificates() } returns certificates
263+
264+
expectThat(subject.certificateByArn(arn))
265+
.get { arn } isEqualTo arn
266+
}
267+
268+
@Test
269+
fun `an unknown certificate ARN throws an exception`() {
270+
every { cloudDriver.getCertificates() } returns certificates
271+
272+
expectThrows<ResourceNotFound> { subject.certificateByArn("does-not-exist") }
273+
}
274+
275+
@ParameterizedTest
276+
@ValueSource(strings = ["arn:cert-1", "arn:cert-2"])
277+
fun `subsequent requests for a certificate by ARN hit the cache`(arn: String) {
278+
every { cloudDriver.getCertificates() } returns certificates
279+
280+
repeat(5) { subject.certificateByArn(arn) }
281+
282+
verify(exactly = 1) { cloudDriver.getCertificates() }
283+
}
284+
285+
@Test
286+
fun `all certs are cached at once when requested by ARN`() {
287+
every { cloudDriver.getCertificates() } returns certificates
288+
289+
listOf("arn:cert-1", "arn:cert-2")
290+
.forEach(subject::certificateByArn)
291+
292+
verify(exactly = 1) { cloudDriver.getCertificates() }
293+
}
214294
}

0 commit comments

Comments
 (0)