Skip to content

Commit

Permalink
feat(keel): upsert and delete intents through stages (spinnaker#1891)
Browse files Browse the repository at this point in the history
* feat(keel): upsert and delete intents through stages
  • Loading branch information
emjburns authored Jan 19, 2018
1 parent 2540270 commit 2f741fc
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 4 deletions.
23 changes: 23 additions & 0 deletions orca-keel/orca-keel.gradle
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.
*/

apply from: "$rootDir/gradle/kotlin.gradle"

dependencies {
compile "com.fasterxml.jackson.module:jackson-module-kotlin:${spinnaker.version("jackson")}"
compile project(":orca-retrofit")
testCompile project(":orca-test")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.orca

import com.netflix.spinnaker.orca.keel.model.UpsertIntentRequest
import retrofit.client.Response
import retrofit.http.Body
import retrofit.http.DELETE
import retrofit.http.GET
import retrofit.http.POST
import retrofit.http.Path
import retrofit.http.Query

interface KeelService {
@GET("/v1/intents/")
fun getIntents(): Response

@POST("/v1/intents/")
fun upsertIntents(@Body upsertIntentRequest: UpsertIntentRequest): Response

@DELETE("/v1/intents/{id}")
fun deleteIntents(@Path("id") id: String, @Query("status") status: String?) : Response
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.orca.config

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.netflix.spinnaker.orca.KeelService
import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import retrofit.Endpoint
import retrofit.Endpoints
import retrofit.RestAdapter
import retrofit.client.Client
import retrofit.converter.JacksonConverter

@Configuration
@ConditionalOnProperty("keel.enabled")
@ComponentScan(
basePackages = [
"com.netflix.spinnaker.orca.keel.task",
"com.netflix.spinnaker.orca.keel.pipeline"
]
)
open class KeelConfiguration {
@Bean open fun keelEndpoint(@Value("\${keel.baseUrl}") keelBaseUrl: String): Endpoint {
return Endpoints.newFixedEndpoint(keelBaseUrl)
}

@Bean open fun keelService(keelEndpoint: Endpoint,
keelObjectMapper: ObjectMapper,
retrofitClient: Client,
retrofitLogLevel: RestAdapter.LogLevel)
= RestAdapter.Builder()
.setEndpoint(keelEndpoint)
.setClient(retrofitClient)
.setLogLevel(retrofitLogLevel)
.setConverter(JacksonConverter(keelObjectMapper))
.build()
.create(KeelService::class.java)


@Bean open fun keelObjectMapper()
= OrcaObjectMapper.newInstance()
.registerModule(KotlinModule())
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.orca.keel.model

data class Intent(
val kind: String,
val schema: String,
val spec: Map<String, Any?>,
val status: String = "ACTIVE",
val labels: Map<String, String> = mapOf(),
val attributes: List<Any> = listOf(),
val policies: List<Any> = listOf(),
val id: String?,
val namespace: String?,
val idOverride: String?
)

data class UpsertIntentRequest(
val intents: List<Intent>,
val dryRun: Boolean
)

data class UpsertIntentResponse(
val intentId: String,
val intentStatus: String
)

data class UpsertIntentDryRunResponse(
val summary: Map<String, Any?>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.orca.keel.pipeline

import com.netflix.spinnaker.orca.Task
import com.netflix.spinnaker.orca.keel.task.DeleteIntentTask
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder
import com.netflix.spinnaker.orca.pipeline.TaskNode
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.springframework.stereotype.Component
import kotlin.reflect.KClass

@Component
class DeleteIntentStage() : StageDefinitionBuilder {
override fun taskGraph(stage: Stage, builder: TaskNode.Builder) {
builder.withTask<DeleteIntentTask>("deleteIntent")
}
}

inline fun <reified T: Task> TaskNode.Builder.withTask(name: String) =
withTask(name, T::class.java)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.orca.keel.pipeline

import com.netflix.spinnaker.orca.Task
import com.netflix.spinnaker.orca.keel.task.UpsertIntentTask
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder
import com.netflix.spinnaker.orca.pipeline.TaskNode
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.springframework.stereotype.Component
import kotlin.reflect.KClass

@Component
class UpsertIntentStage() : StageDefinitionBuilder {
override fun taskGraph(stage: Stage, builder: TaskNode.Builder) {
builder.withTask("upsertIntent", UpsertIntentTask::class)
}

private fun TaskNode.Builder.withTask(name: String, type: KClass<out Task>) =
withTask(name, type.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.orca.keel.task

import com.netflix.spinnaker.orca.ExecutionStatus
import com.netflix.spinnaker.orca.KeelService
import com.netflix.spinnaker.orca.RetryableTask
import com.netflix.spinnaker.orca.TaskResult
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit

@Component
class DeleteIntentTask
@Autowired constructor(
private val keelService: KeelService
) : RetryableTask {
private val log = LoggerFactory.getLogger(javaClass)

override fun execute(stage: Stage) : TaskResult {
if (!stage.context.containsKey("intentId")) {
throw IllegalArgumentException("Missing required task parameter (intentId)")
}

val intentId = stage.context["intentId"].toString()

val status = stage.context["status"]?.toString()

val response = keelService.deleteIntents(intentId, status)

val outputs = mapOf("intent.id" to intentId)

return TaskResult(
if (response.status == HttpStatus.NO_CONTENT.value()) ExecutionStatus.SUCCEEDED else ExecutionStatus.TERMINAL,
outputs
)
}

override fun getBackoffPeriod() = TimeUnit.SECONDS.toMillis(15)

override fun getTimeout() = TimeUnit.MINUTES.toMillis(1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.orca.keel.task

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.orca.ExecutionStatus
import com.netflix.spinnaker.orca.KeelService
import com.netflix.spinnaker.orca.RetryableTask
import com.netflix.spinnaker.orca.TaskResult
import com.netflix.spinnaker.orca.keel.model.Intent
import com.netflix.spinnaker.orca.keel.model.UpsertIntentDryRunResponse
import com.netflix.spinnaker.orca.keel.model.UpsertIntentRequest
import com.netflix.spinnaker.orca.keel.model.UpsertIntentResponse
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit

@Component
class UpsertIntentTask
@Autowired constructor(
private val keelService: KeelService,
private val keelObjectMapper: ObjectMapper
) : RetryableTask {
private val log = LoggerFactory.getLogger(javaClass)

override fun execute(stage: Stage) : TaskResult {
val missingParams = mutableListOf<String>()

if (!stage.context.containsKey("intents")) {
missingParams.add("intents")
}

if (!stage.context.containsKey("dryRun") ) {
missingParams.add("dryRun")
}

if (missingParams.isNotEmpty()) {
throw IllegalArgumentException("Missing required task parameters (${missingParams.joinToString(",")})")
}

val upsertIntentRequest = UpsertIntentRequest(
intents = keelObjectMapper.convertValue(stage.context["intents"], object : TypeReference<List<Intent>>() {}),
dryRun = stage.context["dryRun"].toString().toBoolean()
)

val response = keelService.upsertIntents(upsertIntentRequest)

val outputs = mutableMapOf<String, Any>()

try {
if (upsertIntentRequest.dryRun) {
val dryRunResponse = keelObjectMapper.readValue<List<UpsertIntentDryRunResponse>>(response.body.`in`(), object : TypeReference<List<UpsertIntentDryRunResponse>>(){})
outputs.put("upsertIntentResponse", dryRunResponse)
} else {
val upsertResponse = keelObjectMapper.readValue<List<UpsertIntentResponse>>(response.body.`in`(), object : TypeReference<List<UpsertIntentResponse>>(){})
outputs.put("upsertIntentResponse", upsertResponse)
}
} catch (e: Exception) {
log.error("Error processing upsert intent response from keel", e)
throw e
}

return TaskResult(
if (response.status == HttpStatus.ACCEPTED.value()) ExecutionStatus.SUCCEEDED else ExecutionStatus.TERMINAL,
outputs
)
}

override fun getBackoffPeriod() = TimeUnit.SECONDS.toMillis(15)

override fun getTimeout() = TimeUnit.MINUTES.toMillis(1)

}
Loading

0 comments on commit 2f741fc

Please sign in to comment.