Skip to content

Commit

Permalink
feat(pipeline/kubernetes): Introduce validator for kubernetes blue/gr…
Browse files Browse the repository at this point in the history
…een traffic management strategy (#1176)

Introduce blue/green strategy as this is a terminology used more often in the industry compared to the Neflix specific phrasing. First step is to deprecate redblack. When creating new pipelines using fron50 api, if traffic management is enabled and redblack is used a warning message is logged.

In the future, after the redblack will be completely remove a validation error will be returned
  • Loading branch information
ovidiupopa07 committed Nov 17, 2022
1 parent c214eb5 commit 7b76e6e
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 0 deletions.
@@ -0,0 +1,79 @@
/*
* Copyright 2022 Armory, 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.front50.validator.pipeline;

import com.netflix.spinnaker.front50.api.model.pipeline.Pipeline;
import com.netflix.spinnaker.front50.api.validator.PipelineValidator;
import com.netflix.spinnaker.front50.api.validator.ValidatorErrors;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class KubernetesBlueGreenStrategyValidator implements PipelineValidator {
@Override
public void validate(Pipeline pipeline, ValidatorErrors errors) {

List<Map<String, Object>> stages = pipeline.getStages();

if (stages == null || stages.isEmpty()) {
return;
}

boolean redBlackStrategy =
stages.stream()
.filter(KubernetesBlueGreenStrategyValidator::kubernetesProvider)
.filter(KubernetesBlueGreenStrategyValidator::deployManifestStage)
.map(KubernetesBlueGreenStrategyValidator::getTrafficManagement)
.filter(KubernetesBlueGreenStrategyValidator::trafficManagementEnabled)
.map(KubernetesBlueGreenStrategyValidator::getTrafficManagementOptions)
.anyMatch(KubernetesBlueGreenStrategyValidator::redBlackStrategy);

if (redBlackStrategy) {
log.warn(
"Kubernetes traffic management redblack strategy is deprecated and will be removed soon. Please use bluegreen instead");
// TODO uncomment the line below when we decide to fail the process. also update the test
// errors.reject("Kubernetes traffic management redblack strategy is deprecated and will
// be removed soon. Please use bluegreen instead");
}
}

private static Map<String, Object> getTrafficManagementOptions(
Map<String, Object> trafficManagement) {
return (Map<String, Object>) trafficManagement.get("options");
}

private static boolean trafficManagementEnabled(Map<String, Object> trafficManagement) {
return Boolean.TRUE.equals(trafficManagement.get("enabled"));
}

private static Map<String, Object> getTrafficManagement(Map<String, Object> stage) {
return (Map<String, Object>) stage.get("trafficManagement");
}

private static boolean kubernetesProvider(Map<String, Object> stage) {
return "kubernetes".equals(stage.get("cloudProvider"));
}

private static boolean deployManifestStage(Map<String, Object> stage) {
return "deployManifest".equals(stage.get("type"));
}

private static boolean redBlackStrategy(Map<String, Object> stage) {
return "redblack".equals(stage.get("strategy"));
}
}
@@ -0,0 +1,182 @@
/*
* Copyright 2022 Armory, 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.front50.validator.pipeline

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
import com.netflix.spinnaker.front50.api.model.pipeline.Pipeline
import com.netflix.spinnaker.front50.api.validator.PipelineValidator
import com.netflix.spinnaker.front50.api.validator.ValidatorErrors
import org.slf4j.LoggerFactory
import spock.lang.Specification

class KubernetesBlueGreenStrategyValidatorSpec extends Specification {

private static MemoryAppender memoryAppender

void setup() {
Logger logger = (Logger) LoggerFactory.getLogger("com.netflix.spinnaker.front50.validator.pipeline")
memoryAppender = new MemoryAppender()
memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory())
logger.setLevel(Level.DEBUG)
logger.addAppender(memoryAppender)
memoryAppender.start()
}


void cleanup() {
memoryAppender.reset()
memoryAppender.stop()
}

def "should not return error when stage null or empty"() {
setup:
def pipeline = new Pipeline()
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
!errors.hasErrors()

when:
pipeline.setStages(List.of())
validator.validate(pipeline, errors)

then:
!errors.hasErrors()
}

def "should not return error cloud provider is not Kubernetes"() {
setup:
def pipeline = new Pipeline()
Map<String, Object> trafficManagement = new LinkedHashMap<>()
trafficManagement.put("enabled", true)
Map<String, Object> options = new LinkedHashMap<>()
options
pipeline.setStages(List.of(Map.of("cloudProvider","aws", "type", "deploy")))
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
!errors.hasErrors()
}

def "should not return error when there is no deployManifestStage"() {
setup:
def pipeline = new Pipeline()
Map<String, Object> trafficManagement = new LinkedHashMap<>()
trafficManagement.put("enabled", true)
Map<String, Object> options = new LinkedHashMap<>()
options
pipeline.setStages(List.of(Map.of("cloudProvider","kubernetes", "type", "patchManifest")))
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
!errors.hasErrors()
}

def "should not return error when trafficManagement not enabled"() {
setup:
def pipeline = new Pipeline()
Map<String, Object> trafficManagement = new LinkedHashMap<>()
trafficManagement.put("enabled", true)
Map<String, Object> options = new LinkedHashMap<>()
options
pipeline.setStages(List.of(Map.of("cloudProvider","kubernetes", "type", "deployManifest", "trafficManagement", Map.of("enabled", false))))
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
!errors.hasErrors()
}

def "should not return error when redblack is not selected"() {
setup:
def pipeline = new Pipeline()
Map<String, Object> trafficManagement = new LinkedHashMap<>()
trafficManagement.put("enabled", true)
Map<String, Object> options = new LinkedHashMap<>()
options
pipeline.setStages(List.of(Map.of("cloudProvider","kubernetes", "type", "deployManifest", "trafficManagement", Map.of("enabled", true, "options", Map.of("strategy", "bluegreen")))))
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
!errors.hasErrors()
}

def "should return error when redblack is used"() {
setup:
def pipeline = new Pipeline()
Map<String, Object> trafficManagement = new LinkedHashMap<>()
trafficManagement.put("enabled", true)
Map<String, Object> options = new LinkedHashMap<>()
options
pipeline.setStages(List.of(Map.of("cloudProvider","kubernetes",
"type", "deployManifest",
"trafficManagement", Map.of("enabled", true, "options", Map.of("strategy", "redblack")))))
def errors = new ValidatorErrors()

when:
PipelineValidator validator = new KubernetesBlueGreenStrategyValidator()
validator.validate(pipeline, errors)

then:
memoryAppender.getSize() ==1
memoryAppender.contains("Kubernetes traffic management redblack strategy is deprecated and will be removed soon. Please use bluegreen instead", Level.WARN)
!errors.hasErrors()
// errors.getAllErrors().size() == 1
// errors.getAllErrorsMessage().equals("Kubernetes traffic management redblack strategy is deprecated and will be removed soon. Please use bluegreen instead")
}

class MemoryAppender extends ListAppender<ILoggingEvent> {
void reset() {
this.list.clear();
}
int getSize() {
return this.list.size();
}

boolean contains(String string, Level level) {
return this.list.stream()
.filter({ event ->
event.toString().contains(string)
}).anyMatch( { event ->
event.getLevel().equals(level)
});
}
}
}
Expand Up @@ -23,6 +23,7 @@
import com.netflix.spinnaker.fiat.shared.FiatStatus;
import com.netflix.spinnaker.filters.AuthenticatedRequestFilter;
import com.netflix.spinnaker.front50.ItemDAOHealthIndicator;
import com.netflix.spinnaker.front50.api.validator.PipelineValidator;
import com.netflix.spinnaker.front50.model.application.ApplicationDAO;
import com.netflix.spinnaker.front50.model.application.ApplicationPermissionDAO;
import com.netflix.spinnaker.front50.model.delivery.DeliveryRepository;
Expand All @@ -31,6 +32,7 @@
import com.netflix.spinnaker.front50.model.pipeline.PipelineTemplateDAO;
import com.netflix.spinnaker.front50.model.project.ProjectDAO;
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccountDAO;
import com.netflix.spinnaker.front50.validator.pipeline.KubernetesBlueGreenStrategyValidator;
import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService;
import com.netflix.spinnaker.kork.web.context.AuthenticatedRequestContextProvider;
import com.netflix.spinnaker.kork.web.context.RequestContextProvider;
Expand Down Expand Up @@ -152,4 +154,9 @@ public FiatStatus fiatStatus(
FiatClientConfigurationProperties fiatClientConfigurationProperties) {
return new FiatStatus(registry, dynamicConfigService, fiatClientConfigurationProperties);
}

@Bean
PipelineValidator kubernetesBlueGreenValidator() {
return new KubernetesBlueGreenStrategyValidator();
}
}

0 comments on commit 7b76e6e

Please sign in to comment.