Skip to content

Commit

Permalink
feat(owner): adds primary owners (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
emjburns authored Nov 12, 2018
1 parent 19d5187 commit 7f643ff
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import org.springframework.stereotype.Component

@Component
class AmazonTagOwnerResolutionStrategy : ResourceOwnerResolutionStrategy<Resource> {

override fun primaryFor(): Set<String> = emptySet()

override fun resolve(resource: Resource): String? {
if ("tags" in resource.details) {
(resource.details["tags"] as? List<Map<*, *>>)?.let { tags ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ abstract class AbstractResourceTypeHandler<T : Resource>(
}
}

/**
* Adds the owner to the resourceOwnerField, for each candidate, and if no owner is found it's resolved to the
* "defaultDestination", defined in config
*/
private fun List<T>.withResolvedOwners(workConfiguration: WorkConfiguration): List<T> =
this.map { candidate ->
candidate.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,60 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.util.regex.Pattern

/**
* Looks at all available resolution strategies for finding an owner for a resource,
* and returns all owners.
*/
@Component
open class ResourceOwnerResolver<in T : Resource>(
private val registry: Registry,
private val resourceOwnerResolutionStrategies: List<ResourceOwnerResolutionStrategy<T>>
) : OwnerResolver<T> {
private val resourceOwnerId = registry.createId("swabbie.resources.owner")

override fun resolve(resource: T): String? {
try {
resourceOwnerResolutionStrategies.mapNotNull {
it.resolve(resource)
}.let { owners ->
val emails = findValidEmails(owners)
return if (emails != null) {
registry.counter(
resourceOwnerId.withTags(
"result", "found",
"strategy", javaClass.simpleName,
"resourceType", resource.resourceType
)
).increment()
emails.toSet().joinToString(",")
} else {
registry.counter(
resourceOwnerId.withTags(
"result", "notFound",
"strategy", javaClass.simpleName,
"resourceType", resource.resourceType
)
).increment()
null
val otherOwners = mutableSetOf<String>()
val primaryOwners = mutableSetOf<String>()

resourceOwnerResolutionStrategies.forEach { strategy ->
val owner = strategy.resolve(resource)
owner?.let {
if (strategy.primaryFor().contains(resource.resourceType)) {
primaryOwners.add(owner)
} else {
otherOwners.add(owner)
}
}
}

val validEmails = findValidEmails(if (primaryOwners.isNotEmpty()) primaryOwners else otherOwners)

return if (validEmails.isNotEmpty()) {
registry.counter(
resourceOwnerId.withTags(
"result", "found",
"resourceType", resource.resourceType
)
).increment()
validEmails.joinToString(",")
} else {
registry.counter(
resourceOwnerId.withTags(
"result", "notFound",
"resourceType", resource.resourceType
)
).increment()
null
}
} catch (e: Exception) {
log.info("Failed to find owner for {}.", resource, e)
registry.counter(resourceOwnerId.withTags("result", "failed")).increment()
return null
}
}

private fun findValidEmails(emails: List<String>): List<String> {
private fun findValidEmails(emails: Set<String>): List<String> {
return emails.filter {
Pattern.compile(
"^(([\\w-]+\\.)+[\\w-]+|([a-zA-Z]|[\\w-]{2,}))@"
Expand All @@ -82,6 +96,15 @@ interface OwnerResolver<in T : Resource> {
fun resolve(resource: T): String?
}

/**
* Defines a specific way to find an owner for a resource,
* for example by a tag, from an application, or from an external system
*/
interface ResourceOwnerResolutionStrategy<in T : Resource> {
fun resolve(resource: T): String?

/**
* The resource types for which this resolution strategy knows the primary owner
*/
fun primaryFor(): Set<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ abstract class Resource : Excludable, Timestamped, HasDetails() {
set(name, value)
}

fun toLog() =
"$resourceId:$resourceType"

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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

import com.fasterxml.jackson.annotation.JsonTypeName
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.should.shouldMatch
import com.netflix.spectator.api.NoopRegistry
import com.netflix.spinnaker.swabbie.model.Resource
import org.junit.jupiter.api.Test
import java.time.Instant

object ResourceOwnerResolverTest {

private val resource = TestResource("1")
private val secondResource = TestResource(id = "2", resourceType = "type")

@Test
fun `should resolve to single owner`() {
val subject = ResourceOwnerResolver(NoopRegistry(), listOf(ConstantStrategy()))
subject.resolve(resource) shouldMatch equalTo("[email protected]")
}

@Test
fun `should resolve to null owner`() {
val subject = ResourceOwnerResolver(NoopRegistry(), listOf(NoopStrategy()))
assert(subject.resolve(resource) == null)
}

@Test
fun `should pick primary owner`() {
val subject = ResourceOwnerResolver(NoopRegistry(), listOf(ConstantStrategy(), TestStrategy()))
subject.resolve(resource) shouldMatch equalTo("[email protected]")
}

@Test
fun `should return multiple owners`() {
val subject = ResourceOwnerResolver(NoopRegistry(), listOf(ConstantStrategy(), TestStrategy()))
subject.resolve(secondResource) shouldMatch equalTo("[email protected],[email protected]")
}
}

class NoopStrategy : ResourceOwnerResolutionStrategy<Resource> {
override fun resolve(resource: Resource): String? = null
override fun primaryFor(): Set<String> = emptySet()
}

class ConstantStrategy : ResourceOwnerResolutionStrategy<Resource> {
override fun resolve(resource: Resource): String? = "[email protected]"
override fun primaryFor(): Set<String> = emptySet()
}

class TestStrategy : ResourceOwnerResolutionStrategy<Resource> {
override fun resolve(resource: Resource): String? {
return if (resource.resourceId == "1") {
"[email protected]"
} else {
"[email protected]"
}
}
override fun primaryFor(): Set<String> = setOf("image")
}

@JsonTypeName("Test")
data class TestResource(
private val id: String,
override val resourceId: String = id,
override val name: String = "name",
override val resourceType: String = "image",
override val cloudProvider: String = "provider",
override val createTs: Long = Instant.parse("2018-05-24T12:34:56Z").toEpochMilli()
) : Resource()
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.netflix.spinnaker.swabbie.front50

import com.netflix.spectator.api.Id
import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.moniker.frigga.FriggaReflectiveNamer
import com.netflix.spinnaker.swabbie.InMemoryCache
Expand All @@ -35,6 +34,8 @@ class ApplicationResourceOwnerResolutionStrategy(
private val log: Logger = LoggerFactory.getLogger(this.javaClass)
private val resourceOwnerId = registry.createId("swabbie.resources.owner")

override fun primaryFor(): Set<String> = emptySet()

override fun resolve(resource: Resource): String? {
val applicationToOwnersPairs = mutableSetOf<Pair<String?, String?>>()

Expand Down

0 comments on commit 7f643ff

Please sign in to comment.