Skip to content

Commit

Permalink
Added Interactive component for manual judgement awaited and callback…
Browse files Browse the repository at this point in the history
… handler.
  • Loading branch information
singh09iet authored and yugaa22 committed Sep 25, 2023
1 parent dcc439a commit d964031
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package com.netflix.spinnaker.orca.echo

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import groovy.transform.Canonical
import retrofit.client.Response
import retrofit.http.Body
import retrofit.http.GET
Expand Down Expand Up @@ -46,6 +49,8 @@ interface EchoService {

Source source
Map<String, Object> additionalContext = [:]
InteractiveActions interactiveActions
Boolean useInteractiveBot = false

static class Source {
String executionType
Expand All @@ -71,6 +76,39 @@ interface EchoService {
NORMAL,
HIGH
}
static class InteractiveActions {
String callbackServiceId
String callbackMessageId
String color = '#cccccc'
List<InteractiveAction> actions = []
}

@JsonTypeInfo(
include = JsonTypeInfo.As.EXISTING_PROPERTY,
use = JsonTypeInfo.Id.NAME,
property = "type")
@JsonSubTypes(
@JsonSubTypes.Type(value = ButtonAction.class, name = "button")
)
abstract static class InteractiveAction {
String type
String name
String value
}

@Canonical
static class ButtonAction extends InteractiveAction {
String type = "button"
String label
}

@Canonical
static class InteractiveActionCallback {
InteractiveAction actionPerformed
String serviceId
String messageId
String user
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2022 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.echo.notification

import com.google.common.base.Preconditions
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType
import com.netflix.spinnaker.orca.api.pipeline.models.PipelineExecution
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution
import com.netflix.spinnaker.orca.pipeline.CompoundExecutionOperator
import com.netflix.spinnaker.security.AuthenticatedRequest
import org.apache.commons.lang3.StringUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Component

import static com.netflix.spinnaker.orca.echo.EchoService.Notification.InteractiveActionCallback

@Component
class ManualJudgementCallbackHandler {
@Autowired
CompoundExecutionOperator executionOperator

ResponseEntity<String> processCallback(RequestEntity<InteractiveActionCallback> request) {
InteractiveActionCallback interactiveActionCallback = request.getBody()
String judgement = interactiveActionCallback.getActionPerformed().getValue()
String messageId = interactiveActionCallback.getMessageId()
String user = interactiveActionCallback.getUser()
String[] ids = StringUtils.split(messageId, "-")
Preconditions.checkArgument(ids.length == 3, "Unexpected message id")
ExecutionType executionType = getExecutionType(ids[0])
String executionId = ids[1]
String stageId = ids[2]
PipelineExecution pipelineExecution = executionOperator.updateStage(executionType, executionId, stageId,
{ stage ->
stage.context.putAll(Map.of("judgmentStatus", judgement.toLowerCase()))
stage.lastModified = new StageExecution.LastModifiedDetails(
user: user,
allowedAccounts: AuthenticatedRequest.getSpinnakerAccounts().orElse(null)?.split(",") ?: [],
lastModifiedTime: System.currentTimeMillis()
)
stage.context["lastModifiedBy"] = stage.lastModified.user
})
String pipelineName = pipelineExecution.getName()
return ResponseEntity.ok("pipeline $pipelineName updated successfully") as ResponseEntity<String>
}

private static ExecutionType getExecutionType(String name) {
for (ExecutionType executionType : ExecutionType.values()) {
if (executionType.toString().equalsIgnoreCase(name)) {
return executionType;
}
}
throw new IllegalArgumentException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ package com.netflix.spinnaker.orca.echo.pipeline
import com.fasterxml.jackson.annotation.JsonAnyGetter
import com.fasterxml.jackson.annotation.JsonAnySetter
import com.fasterxml.jackson.annotation.JsonIgnore
import com.google.common.collect.ImmutableList
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.base.Strings
import com.netflix.spinnaker.fiat.shared.FiatStatus
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus
import com.netflix.spinnaker.orca.api.pipeline.OverridableTimeoutRetryableTask
import com.netflix.spinnaker.orca.api.pipeline.models.PipelineExecution
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution
import com.netflix.spinnaker.orca.api.pipeline.TaskResult
import com.netflix.spinnaker.orca.echo.util.ManualJudgmentAuthorization
import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl

import javax.annotation.Nonnull
import java.util.concurrent.TimeUnit
Expand All @@ -37,6 +41,7 @@ import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import static com.netflix.spinnaker.orca.echo.EchoService.Notification.InteractiveActions

@Component
class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage {
Expand All @@ -63,13 +68,13 @@ class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage
return Optional.of(
new PipelineExecution.AuthenticationDetails(
stage.lastModified.user,
stage.lastModified.allowedAccounts));
stage.lastModified.allowedAccounts))
}

@Slf4j
@Component
@VisibleForTesting
public static class WaitForManualJudgmentTask implements OverridableTimeoutRetryableTask {
static class WaitForManualJudgmentTask implements OverridableTimeoutRetryableTask {
final long backoffPeriod = 15000
final long timeout = TimeUnit.DAYS.toMillis(3)

Expand Down Expand Up @@ -219,6 +224,7 @@ class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage
}

void notify(EchoService echoService, StageExecution stage, String notificationState) {
boolean useInteractiveBot = ("manualJudgment".equalsIgnoreCase(notificationState))
echoService.create(new EchoService.Notification(
notificationType: EchoService.Notification.Type.valueOf(type.toUpperCase()),
to: address ? [address] : (publisherName ? [publisherName] : null),
Expand All @@ -240,9 +246,31 @@ class ManualJudgmentStage implements StageDefinitionBuilder, AuthenticatedStage
judgmentInputs : stage.context.judgmentInputs,
judgmentInput : stage.context.judgmentInput,
judgedBy : stage.context.lastModifiedBy
]
],
useInteractiveBot: useInteractiveBot,
interactiveActions: useInteractiveBot ? getInteractiveActions(stage) : null
))
lastNotifiedByNotificationState[notificationState] = new Date()
}

private static InteractiveActions getInteractiveActions(StageExecution stage) {
new InteractiveActions(
callbackServiceId: "orca",
callbackMessageId: "${stage.getExecution().getType()}-${stage.getExecution().getId()}-${stage.getId()}",
color: '#fcba03',
actions: ImmutableList.of(
new EchoService.Notification.ButtonAction(
name: "manual-judgement",
label: "Approve",
value: StageData.State.CONTINUE.name()
),
new EchoService.Notification.ButtonAction(
name: "manual-judgement",
label: "Reject",
value: StageData.State.STOP.name()
)
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2022 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.controllers

import com.netflix.spinnaker.orca.echo.notification.ManualJudgementCallbackHandler
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController

import static com.netflix.spinnaker.orca.echo.EchoService.Notification.InteractiveActionCallback

@RequestMapping("/notifications")
@RestController
@Slf4j
class NotificationCallbackController {

@Autowired
ManualJudgementCallbackHandler manualJudgementCallbackHandler;

@RequestMapping(
value = "/callback",
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<String> processCallback(RequestEntity<InteractiveActionCallback> request) {
return manualJudgementCallbackHandler.processCallback(request)
}
}

0 comments on commit d964031

Please sign in to comment.