Skip to content

Commit

Permalink
feat(core): Policies (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert authored Nov 7, 2017
1 parent 97e88d5 commit f33ae30
Show file tree
Hide file tree
Showing 21 changed files with 368 additions and 44 deletions.
2 changes: 2 additions & 0 deletions keel-core/keel-core.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ dependencies {

compile "com.fasterxml.jackson.module:jackson-module-kotlin:${spinnaker.version("jackson")}"
compile 'com.github.jonpeterson:jackson-module-model-versioning:1.2.2'

testCompile project(":keel-test")
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.util.ClassUtil
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.github.jonpeterson.jackson.module.versioning.VersioningModule
import com.netflix.spinnaker.keel.Intent
import com.netflix.spinnaker.keel.IntentActivityRepository
import com.netflix.spinnaker.keel.IntentRepository
import com.netflix.spinnaker.keel.IntentSpec
import com.netflix.spinnaker.keel.*
import com.netflix.spinnaker.keel.memory.MemoryIntentActivityRepository
import com.netflix.spinnaker.keel.memory.MemoryIntentRepository
import com.netflix.spinnaker.keel.memory.MemoryTraceRepository
Expand All @@ -38,6 +35,7 @@ import org.springframework.context.annotation.Configuration
import org.springframework.core.type.filter.AssignableTypeFilter
import org.springframework.util.ClassUtils
import java.time.Clock
import kotlin.reflect.KClass

@Configuration
@ComponentScan(basePackages = arrayOf(
Expand All @@ -50,35 +48,24 @@ open class KeelConfiguration {
@Autowired
open fun objectMapper(objectMapper: ObjectMapper) {
objectMapper.apply {
registerSubtypes(*findAllIntentSubtypes().toTypedArray())
registerSubtypes(*findAllIntentSpecSubtypes().toTypedArray())
registerSubtypes(*findAllSubtypes(Intent::class.java, "com.netflix.spinnaker.keel.intents").toTypedArray())
registerSubtypes(*findAllSubtypes(IntentSpec::class.java, "com.netflix.spinnaker.keel.intents").toTypedArray())
registerSubtypes(*findAllSubtypes(Policy::class.java, "com.netflix.spinnaker.keel.policy").toTypedArray())
}
.registerModule(KotlinModule())
.registerModule(VersioningModule())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}

private fun findAllIntentSubtypes(): List<Class<*>> {
return ClassPathScanningCandidateComponentProvider(false)
.apply { addIncludeFilter(AssignableTypeFilter(Intent::class.java)) }
.findCandidateComponents("com.netflix.spinnaker.keel.intents")
.map {
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
log.info("Registering Intent: ${cls.simpleName}")
return@map cls
}
}

private fun findAllIntentSpecSubtypes(): List<Class<*>> {
return ClassPathScanningCandidateComponentProvider(false)
.apply { addIncludeFilter(AssignableTypeFilter(IntentSpec::class.java)) }
.findCandidateComponents("com.netflix.spinnaker.keel.intents")
.map {
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
log.info("Registering IntentSpec: ${cls.simpleName}")
return@map cls
}
}
private fun findAllSubtypes(clazz: Class<*>, pkg: String): List<Class<*>>
= ClassPathScanningCandidateComponentProvider(false)
.apply { addIncludeFilter(AssignableTypeFilter(clazz)) }
.findCandidateComponents(pkg)
.map {
val cls = ClassUtils.resolveClassName(it.beanClassName, ClassUtils.getDefaultClassLoader())
log.info("Registering ${cls.simpleName}")
return@map cls
}

@Bean
@ConditionalOnMissingBean(IntentRepository::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ abstract class Intent<out S : IntentSpec>
@JsonCreator constructor(
@JsonSerializeToVersion(defaultToSource = true) val schema: String,
val kind: String,
val spec: S
val spec: S,
val status: IntentStatus = IntentStatus.ACTIVE,
val policies: List<Policy> = listOf()
) {

val status: IntentStatus = IntentStatus.ACTIVE

abstract fun getId(): String

@JsonIgnore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,3 @@ interface IntentLauncher<out R : LaunchedIntentResult> {
}

interface LaunchedIntentResult

// TODO rz - This actually shouldn't be used, but still riffing on the interface
class StubIntentResult : LaunchedIntentResult
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2017 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

import com.fasterxml.jackson.annotation.JsonTypeInfo

/**
* A Policy can be attached to a single Intent, or applied globally via Keel configuration / dynamic admin API. These
* classes are used to define additional behavior of how an Intent should behave under specific conditions, whether
* an Intent should be applied given the condition of the managed system and/or Spinnaker state, and so-on.
*
* Matchers are primarily attached to policies only on Keel configuration / admin API usage, allowing a policy to be
* applied globally, matching a subset of Intents. For example, an EnabledPolicy could be set with a falsey value, and
* use Matchers to narrow by the PriorityMatcher so that only CRITICAL Priority Intents are enabled, across all
* applications.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
abstract class Policy {
fun getId(): String = this.javaClass.simpleName
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2017 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

enum class IntentPriority {
CRITICAL, HIGH, NORMAL, LOW
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.netflix.spinnaker.keel

import com.netflix.spinnaker.keel.matcher.Matcher

interface IntentRepository {

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

fun getIntent(id: String): Intent<IntentSpec>?

fun findByMatch(matchers: List<Matcher>)
= getIntents().filter { i ->
matchers.any { m -> m.match(i) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ package com.netflix.spinnaker.keel
* A typed model of an Intent's configuration.
*/
interface IntentSpec

abstract class ApplicationAwareIntentSpec : IntentSpec {
abstract val application: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2017 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

interface PolicyRepository {

fun findAll(): List<Policy>
fun upsert(policy: Policy)
fun delete(id: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2017 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.matcher

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonTypeName
import com.netflix.spinnaker.keel.ApplicationAwareIntentSpec
import com.netflix.spinnaker.keel.Intent
import com.netflix.spinnaker.keel.IntentPriority
import com.netflix.spinnaker.keel.IntentSpec
import com.netflix.spinnaker.keel.policy.PriorityPolicy

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind")
interface Matcher {
fun match(intent: Intent<IntentSpec>): Boolean
}

@JsonTypeName("All")
class AllMatcher : Matcher {
override fun match(intent: Intent<IntentSpec>): Boolean {
return true
}
}

@JsonTypeName("Application")
class ApplicationMatcher(val expected: String) : Matcher {
override fun match(intent: Intent<IntentSpec>) =
when (intent.spec) {
is ApplicationAwareIntentSpec -> intent.spec.application == expected
else -> false
}
}

enum class PriorityMatcherScope {
EQUAL, EQUAL_GT, EQUAL_LT
}

// TODO rz - allow defaulting intents if the priority policy isn't present
@JsonTypeName("Priority")
class PriorityMatcher(
private val level: IntentPriority,
private val scope: PriorityMatcherScope
) : Matcher {
override fun match(intent: Intent<IntentSpec>)
= intent.policies
.filterIsInstance<PriorityPolicy>()
.filter { p ->
when (scope) {
PriorityMatcherScope.EQUAL -> p.priority == level
PriorityMatcherScope.EQUAL_GT -> p.priority >= level
PriorityMatcherScope.EQUAL_LT -> p.priority <= level
}
}
.count() > 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2017 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.memory

import com.netflix.spinnaker.keel.Policy
import com.netflix.spinnaker.keel.PolicyRepository

class MemoryPolicyRepository : PolicyRepository {

private val policies: MutableMap<String, Policy> = mutableMapOf()

override fun findAll() = policies.entries.map { it.value }

override fun upsert(policy: Policy) {
policies.put(policy.getId(), policy)
}

override fun delete(id: String) {
policies.remove(id)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017 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.policy

import com.fasterxml.jackson.annotation.JsonTypeName
import com.netflix.spinnaker.keel.IntentPriority
import com.netflix.spinnaker.keel.Policy

@JsonTypeName("Enabled")
data class EnabledPolicy(
val flag: Boolean = true
) : Policy()

/**
* PriorityPolicy can be provided to an Intent to assign criticality. This allows end-users to self-define how mandatory
* certain intents are compared to others.
*
* TODO rz - Allow users to self-manage priority of intents inside of their own defined buckets (like, by team).
*/
@JsonTypeName("Priority")
data class PriorityPolicy(
val priority: IntentPriority = IntentPriority.NORMAL
) : Policy()

//@JsonTypeName("Delivery")
//data class DeliveryPolicy(
// val backoffMultiplier: Float,
// val convergeRate: Duration
//) : Policy()
//
//// TODO rz - kinds: "PreviousStateRollback" "RunOrchestrationRollback" "RunPipelineRollback" etc
//@JsonTypeName("Rollback")
//data class RollbackPolicy(
//) : Policy()
//
//@JsonTypeName("ExecutionWindow")
//data class ExecutionWindowPolicy(
// override val matchers: MutableList<Matcher> = mutableListOf()
//) : Policy()

// TODO rz - Allow people to define if they're notified on changes, failures (how to configure notification channels?)
// Probably warrants a keel-echo module and just drop this in there.
//@JsonTypeName("Notification")
//data class NotificationPolicy(
// val changes: Boolean,
// val failures: Boolean
//) : Policy()
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.netflix.spinnaker.keel.ConvergeResult
import com.netflix.spinnaker.keel.Intent
import com.netflix.spinnaker.keel.IntentProcessor
import com.netflix.spinnaker.keel.IntentSpec
import com.netflix.spinnaker.keel.IntentStatus
import com.netflix.spinnaker.keel.model.Job
import com.netflix.spinnaker.keel.model.OrchestrationRequest
import com.netflix.spinnaker.keel.model.Trigger
Expand Down Expand Up @@ -71,7 +72,7 @@ class DryRunIntentLauncherSpec extends Specification {
@NotNull String schema,
@NotNull String kind,
@NotNull TestIntentSpec spec) {
super(schema, kind, spec)
super(schema, kind, spec, IntentStatus.ACTIVE, [])
}

@Override
Expand Down
Loading

0 comments on commit f33ae30

Please sign in to comment.