Skip to content

Commit f33ae30

Browse files
authored
feat(core): Policies (#6)
1 parent 97e88d5 commit f33ae30

File tree

21 files changed

+368
-44
lines changed

21 files changed

+368
-44
lines changed

keel-core/keel-core.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ dependencies {
2323

2424
compile "com.fasterxml.jackson.module:jackson-module-kotlin:${spinnaker.version("jackson")}"
2525
compile 'com.github.jonpeterson:jackson-module-model-versioning:1.2.2'
26+
27+
testCompile project(":keel-test")
2628
}

keel-core/src/main/kotlin/com/netflix/spinnaker/config/KeelConfiguration.kt

+14-27
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
2020
import com.fasterxml.jackson.databind.util.ClassUtil
2121
import com.fasterxml.jackson.module.kotlin.KotlinModule
2222
import com.github.jonpeterson.jackson.module.versioning.VersioningModule
23-
import com.netflix.spinnaker.keel.Intent
24-
import com.netflix.spinnaker.keel.IntentActivityRepository
25-
import com.netflix.spinnaker.keel.IntentRepository
26-
import com.netflix.spinnaker.keel.IntentSpec
23+
import com.netflix.spinnaker.keel.*
2724
import com.netflix.spinnaker.keel.memory.MemoryIntentActivityRepository
2825
import com.netflix.spinnaker.keel.memory.MemoryIntentRepository
2926
import com.netflix.spinnaker.keel.memory.MemoryTraceRepository
@@ -38,6 +35,7 @@ import org.springframework.context.annotation.Configuration
3835
import org.springframework.core.type.filter.AssignableTypeFilter
3936
import org.springframework.util.ClassUtils
4037
import java.time.Clock
38+
import kotlin.reflect.KClass
4139

4240
@Configuration
4341
@ComponentScan(basePackages = arrayOf(
@@ -50,35 +48,24 @@ open class KeelConfiguration {
5048
@Autowired
5149
open fun objectMapper(objectMapper: ObjectMapper) {
5250
objectMapper.apply {
53-
registerSubtypes(*findAllIntentSubtypes().toTypedArray())
54-
registerSubtypes(*findAllIntentSpecSubtypes().toTypedArray())
51+
registerSubtypes(*findAllSubtypes(Intent::class.java, "com.netflix.spinnaker.keel.intents").toTypedArray())
52+
registerSubtypes(*findAllSubtypes(IntentSpec::class.java, "com.netflix.spinnaker.keel.intents").toTypedArray())
53+
registerSubtypes(*findAllSubtypes(Policy::class.java, "com.netflix.spinnaker.keel.policy").toTypedArray())
5554
}
5655
.registerModule(KotlinModule())
5756
.registerModule(VersioningModule())
5857
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
5958
}
6059

61-
private fun findAllIntentSubtypes(): List<Class<*>> {
62-
return ClassPathScanningCandidateComponentProvider(false)
63-
.apply { addIncludeFilter(AssignableTypeFilter(Intent::class.java)) }
64-
.findCandidateComponents("com.netflix.spinnaker.keel.intents")
65-
.map {
66-
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
67-
log.info("Registering Intent: ${cls.simpleName}")
68-
return@map cls
69-
}
70-
}
71-
72-
private fun findAllIntentSpecSubtypes(): List<Class<*>> {
73-
return ClassPathScanningCandidateComponentProvider(false)
74-
.apply { addIncludeFilter(AssignableTypeFilter(IntentSpec::class.java)) }
75-
.findCandidateComponents("com.netflix.spinnaker.keel.intents")
76-
.map {
77-
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
78-
log.info("Registering IntentSpec: ${cls.simpleName}")
79-
return@map cls
80-
}
81-
}
60+
private fun findAllSubtypes(clazz: Class<*>, pkg: String): List<Class<*>>
61+
= ClassPathScanningCandidateComponentProvider(false)
62+
.apply { addIncludeFilter(AssignableTypeFilter(clazz)) }
63+
.findCandidateComponents(pkg)
64+
.map {
65+
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
66+
log.info("Registering ${cls.simpleName}")
67+
return@map cls
68+
}
8269

8370
@Bean
8471
@ConditionalOnMissingBean(IntentRepository::class)

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/Intent.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ abstract class Intent<out S : IntentSpec>
2626
@JsonCreator constructor(
2727
@JsonSerializeToVersion(defaultToSource = true) val schema: String,
2828
val kind: String,
29-
val spec: S
29+
val spec: S,
30+
val status: IntentStatus = IntentStatus.ACTIVE,
31+
val policies: List<Policy> = listOf()
3032
) {
3133

32-
val status: IntentStatus = IntentStatus.ACTIVE
33-
3434
abstract fun getId(): String
3535

3636
@JsonIgnore

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/IntentLauncher.kt

-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,3 @@ interface IntentLauncher<out R : LaunchedIntentResult> {
3232
}
3333

3434
interface LaunchedIntentResult
35-
36-
// TODO rz - This actually shouldn't be used, but still riffing on the interface
37-
class StubIntentResult : LaunchedIntentResult
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel
17+
18+
import com.fasterxml.jackson.annotation.JsonTypeInfo
19+
20+
/**
21+
* A Policy can be attached to a single Intent, or applied globally via Keel configuration / dynamic admin API. These
22+
* classes are used to define additional behavior of how an Intent should behave under specific conditions, whether
23+
* an Intent should be applied given the condition of the managed system and/or Spinnaker state, and so-on.
24+
*
25+
* Matchers are primarily attached to policies only on Keel configuration / admin API usage, allowing a policy to be
26+
* applied globally, matching a subset of Intents. For example, an EnabledPolicy could be set with a falsey value, and
27+
* use Matchers to narrow by the PriorityMatcher so that only CRITICAL Priority Intents are enabled, across all
28+
* applications.
29+
*/
30+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
31+
abstract class Policy {
32+
fun getId(): String = this.javaClass.simpleName
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel
17+
18+
enum class IntentPriority {
19+
CRITICAL, HIGH, NORMAL, LOW
20+
}

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/IntentRepository.kt

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

18+
import com.netflix.spinnaker.keel.matcher.Matcher
19+
1820
interface IntentRepository {
1921

2022
fun upsertIntent(intent: Intent<IntentSpec>): Intent<IntentSpec>
@@ -24,4 +26,9 @@ interface IntentRepository {
2426
fun getIntents(status: List<IntentStatus>): List<Intent<IntentSpec>>
2527

2628
fun getIntent(id: String): Intent<IntentSpec>?
29+
30+
fun findByMatch(matchers: List<Matcher>)
31+
= getIntents().filter { i ->
32+
matchers.any { m -> m.match(i) }
33+
}
2734
}

keel-core/src/main/kotlin/com/netflix/spinnaker/keel/IntentSpec.kt

+4
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ package com.netflix.spinnaker.keel
1919
* A typed model of an Intent's configuration.
2020
*/
2121
interface IntentSpec
22+
23+
abstract class ApplicationAwareIntentSpec : IntentSpec {
24+
abstract val application: String
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel
17+
18+
interface PolicyRepository {
19+
20+
fun findAll(): List<Policy>
21+
fun upsert(policy: Policy)
22+
fun delete(id: String)
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel.matcher
17+
18+
import com.fasterxml.jackson.annotation.JsonTypeInfo
19+
import com.fasterxml.jackson.annotation.JsonTypeName
20+
import com.netflix.spinnaker.keel.ApplicationAwareIntentSpec
21+
import com.netflix.spinnaker.keel.Intent
22+
import com.netflix.spinnaker.keel.IntentPriority
23+
import com.netflix.spinnaker.keel.IntentSpec
24+
import com.netflix.spinnaker.keel.policy.PriorityPolicy
25+
26+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
27+
interface Matcher {
28+
fun match(intent: Intent<IntentSpec>): Boolean
29+
}
30+
31+
@JsonTypeName("All")
32+
class AllMatcher : Matcher {
33+
override fun match(intent: Intent<IntentSpec>): Boolean {
34+
return true
35+
}
36+
}
37+
38+
@JsonTypeName("Application")
39+
class ApplicationMatcher(val expected: String) : Matcher {
40+
override fun match(intent: Intent<IntentSpec>) =
41+
when (intent.spec) {
42+
is ApplicationAwareIntentSpec -> intent.spec.application == expected
43+
else -> false
44+
}
45+
}
46+
47+
enum class PriorityMatcherScope {
48+
EQUAL, EQUAL_GT, EQUAL_LT
49+
}
50+
51+
// TODO rz - allow defaulting intents if the priority policy isn't present
52+
@JsonTypeName("Priority")
53+
class PriorityMatcher(
54+
private val level: IntentPriority,
55+
private val scope: PriorityMatcherScope
56+
) : Matcher {
57+
override fun match(intent: Intent<IntentSpec>)
58+
= intent.policies
59+
.filterIsInstance<PriorityPolicy>()
60+
.filter { p ->
61+
when (scope) {
62+
PriorityMatcherScope.EQUAL -> p.priority == level
63+
PriorityMatcherScope.EQUAL_GT -> p.priority >= level
64+
PriorityMatcherScope.EQUAL_LT -> p.priority <= level
65+
}
66+
}
67+
.count() > 0
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel.memory
17+
18+
import com.netflix.spinnaker.keel.Policy
19+
import com.netflix.spinnaker.keel.PolicyRepository
20+
21+
class MemoryPolicyRepository : PolicyRepository {
22+
23+
private val policies: MutableMap<String, Policy> = mutableMapOf()
24+
25+
override fun findAll() = policies.entries.map { it.value }
26+
27+
override fun upsert(policy: Policy) {
28+
policies.put(policy.getId(), policy)
29+
}
30+
31+
override fun delete(id: String) {
32+
policies.remove(id)
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2017 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.netflix.spinnaker.keel.policy
17+
18+
import com.fasterxml.jackson.annotation.JsonTypeName
19+
import com.netflix.spinnaker.keel.IntentPriority
20+
import com.netflix.spinnaker.keel.Policy
21+
22+
@JsonTypeName("Enabled")
23+
data class EnabledPolicy(
24+
val flag: Boolean = true
25+
) : Policy()
26+
27+
/**
28+
* PriorityPolicy can be provided to an Intent to assign criticality. This allows end-users to self-define how mandatory
29+
* certain intents are compared to others.
30+
*
31+
* TODO rz - Allow users to self-manage priority of intents inside of their own defined buckets (like, by team).
32+
*/
33+
@JsonTypeName("Priority")
34+
data class PriorityPolicy(
35+
val priority: IntentPriority = IntentPriority.NORMAL
36+
) : Policy()
37+
38+
//@JsonTypeName("Delivery")
39+
//data class DeliveryPolicy(
40+
// val backoffMultiplier: Float,
41+
// val convergeRate: Duration
42+
//) : Policy()
43+
//
44+
//// TODO rz - kinds: "PreviousStateRollback" "RunOrchestrationRollback" "RunPipelineRollback" etc
45+
//@JsonTypeName("Rollback")
46+
//data class RollbackPolicy(
47+
//) : Policy()
48+
//
49+
//@JsonTypeName("ExecutionWindow")
50+
//data class ExecutionWindowPolicy(
51+
// override val matchers: MutableList<Matcher> = mutableListOf()
52+
//) : Policy()
53+
54+
// TODO rz - Allow people to define if they're notified on changes, failures (how to configure notification channels?)
55+
// Probably warrants a keel-echo module and just drop this in there.
56+
//@JsonTypeName("Notification")
57+
//data class NotificationPolicy(
58+
// val changes: Boolean,
59+
// val failures: Boolean
60+
//) : Policy()

keel-core/src/test/groovy/com/netflix/spinnaker/keel/dryrun/DryRunIntentLauncherSpec.groovy

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.netflix.spinnaker.keel.ConvergeResult
2222
import com.netflix.spinnaker.keel.Intent
2323
import com.netflix.spinnaker.keel.IntentProcessor
2424
import com.netflix.spinnaker.keel.IntentSpec
25+
import com.netflix.spinnaker.keel.IntentStatus
2526
import com.netflix.spinnaker.keel.model.Job
2627
import com.netflix.spinnaker.keel.model.OrchestrationRequest
2728
import com.netflix.spinnaker.keel.model.Trigger
@@ -71,7 +72,7 @@ class DryRunIntentLauncherSpec extends Specification {
7172
@NotNull String schema,
7273
@NotNull String kind,
7374
@NotNull TestIntentSpec spec) {
74-
super(schema, kind, spec)
75+
super(schema, kind, spec, IntentStatus.ACTIVE, [])
7576
}
7677

7778
@Override

0 commit comments

Comments
 (0)