Skip to content

Commit

Permalink
feat(pipelinetemplate): Basic template & config validation (#1221)
Browse files Browse the repository at this point in the history
  • Loading branch information
robzienert committed Mar 14, 2017
1 parent ee7a401 commit 4c0082d
Show file tree
Hide file tree
Showing 16 changed files with 578 additions and 42 deletions.
Expand Up @@ -30,6 +30,10 @@
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.PipelineTemplate;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.TemplateConfiguration;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.render.Renderer;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.validator.V1TemplateConfigurationSchemaValidator;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.validator.V1TemplateSchemaValidator;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors.Error;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -68,11 +72,11 @@ public Map<String, Object> process(Map<String, Object> pipeline) {
try {
return processInternal(pipeline);
} catch (TemplateLoaderException e) {
return ErrorResponse.builder().addError("failed loading template", e.getMessage()).toMap();
return new Errors().addError(Error.builder().withMessage("failed loading template").withCause(e.getMessage())).toResponse();
} catch (TemplateRenderException e) {
return ErrorResponse.builder().addError("failed rendering handlebars template", e.getMessage()).toMap();
return new Errors().addError(Error.builder().withMessage("failed rendering handlebars template").withCause(e.getMessage())).toResponse();
} catch (IllegalTemplateConfigurationException e) {
return ErrorResponse.builder().addError("malformed template configuration", e.getMessage()).toMap();
return new Errors().addError(Error.builder().withMessage("malformed template configuration").withCause(e.getMessage())).toResponse();
}
}

Expand All @@ -82,38 +86,38 @@ private Map<String, Object> processInternal(Map<String, Object> pipeline) {
return pipeline;
}

Errors validationErrors = new Errors();

TemplateConfiguration templateConfiguration = request.getConfig();
new V1TemplateConfigurationSchemaValidator().validate(templateConfiguration, validationErrors);
if (validationErrors.hasErrors(request.plan)) {
return validationErrors.toResponse();
}

List<PipelineTemplate> templates = templateLoader.load(templateConfiguration.getPipeline().getTemplate());

PipelineTemplate template = TemplateMerge.merge(templates);
Map<String, Object> trigger = (HashMap<String, Object>) pipeline.get("trigger");

new V1TemplateSchemaValidator().validate(template, validationErrors);
if (validationErrors.hasErrors(request.plan)) {
return validationErrors.toResponse();
}

Map<String, Object> trigger = (HashMap<String, Object>) pipeline.get("trigger");
GraphMutator graphMutator = new GraphMutator(templateConfiguration, renderer, registry, trigger);
graphMutator.mutate(template);

// TODO validation

ExecutionGenerator executionGenerator = new V1SchemaExecutionGenerator();

Map<String, Object> generatedPipeline = executionGenerator.generate(template, templateConfiguration);

validateGeneratedPipeline(generatedPipeline);

return generatedPipeline;
}

private static void validateGeneratedPipeline(Map<String, Object> generatedPipeline) {
if (generatedPipeline.get("application") == null) {
throw new IllegalTemplateConfigurationException("Configuration is missing 'application' value");
}
}

private static class TemplatedPipelineRequest {
String type;
Map<String, Object> trigger;
TemplateConfiguration config;
Boolean plan;
Boolean plan = false;

public boolean isTemplatedPipelineRequest() {
return "templatedPipeline".equals(type);
Expand Down Expand Up @@ -151,23 +155,4 @@ public void setPlan(Boolean plan) {
this.plan = plan;
}
}

private static class ErrorResponse {
Map<String, String> errors = new HashMap<>();

static ErrorResponse builder() {
return new ErrorResponse();
}

ErrorResponse addError(String message, String cause) {
errors.put(message, cause);
return this;
}

Map<String, Object> toMap() {
Map<String, Object> m = new HashMap<>();
m.put("errors", errors);
return m;
}
}
}
@@ -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.pipelinetemplate.exceptions;

import java.util.List;
import java.util.Map;

public class InvalidPipelineTemplateException extends RuntimeException {

private List<Map<String, Object>> errors;

public InvalidPipelineTemplateException(List<Map<String, Object>> errors) {
this.errors = errors;
}

public InvalidPipelineTemplateException(String message, List<Map<String, Object>> errors) {
super(message);
this.errors = errors;
}

public List<Map<String, Object>> getErrors() {
return errors;
}
}
Expand Up @@ -16,14 +16,15 @@
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model;

import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.PipelineTemplateVisitor;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.VersionedSchema;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class PipelineTemplate {
public class PipelineTemplate implements VersionedSchema {

private String schema;
private String id;
Expand Down Expand Up @@ -112,6 +113,11 @@ public void setNotifications(List<NamedHashMap> notifications) {
}
}

@Override
public String getSchemaVersion() {
return schema;
}

public String getSchema() {
return schema;
}
Expand Down
Expand Up @@ -15,14 +15,16 @@
*/
package com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model;

import com.netflix.spinnaker.orca.pipelinetemplate.validator.VersionedSchema;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

public class TemplateConfiguration {
public class TemplateConfiguration implements VersionedSchema {

private String schema;
private String id;
Expand Down Expand Up @@ -153,6 +155,11 @@ public void setDescription(String description) {
}
}

@Override
public String getSchemaVersion() {
return schema;
}

public String getRuntimeId() {
return runtimeId;
}
Expand Down
@@ -0,0 +1,51 @@
/*
* 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.pipelinetemplate.v1schema.validator;

import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.StageDefinition;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors.Error;

import java.util.List;
import java.util.function.Function;

public class V1SchemaValidationHelper {

static void validateStageDefinitions(List<StageDefinition> stageDefinitions, Errors errors, Function<String, String> locationFormatter) {
stageDefinitions.forEach(stageDefinition -> {
if (stageDefinition.getId() == null) {
errors.addError(Error.builder()
.withMessage("Stage ID is unset")
.withLocation(locationFormatter.apply("stages"))
);
}

if (stageDefinition.getType() == null) {
errors.addError(Error.builder()
.withMessage("Stage is missing type")
.withLocation(locationFormatter.apply("stages." + stageDefinition.getId()))
);
}

if (stageDefinition.getConfig() == null) {
errors.addError(Error.builder()
.withMessage("Stage configuration is unset")
.withLocation(locationFormatter.apply("stages." + stageDefinition.getId()))
);
}
});
}
}
@@ -0,0 +1,64 @@
/*
* 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.pipelinetemplate.v1schema.validator;

import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.TemplateConfiguration;
import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.TemplateConfiguration.PipelineDefinition;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors.Error;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.SchemaValidator;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.VersionedSchema;

public class V1TemplateConfigurationSchemaValidator implements SchemaValidator {

private static final String SUPPORTED_VERSION = "1";

@Override
public void validate(VersionedSchema configuration, Errors errors) {
if (!(configuration instanceof TemplateConfiguration)) {
throw new IllegalArgumentException("Expected TemplateConfiguration");
}
TemplateConfiguration config = (TemplateConfiguration) configuration;

if (!SUPPORTED_VERSION.equals(config.getSchemaVersion())) {
errors.addError(Error.builder()
.withMessage("config schema version is unsupported: expected '" + SUPPORTED_VERSION + "', got '" + config.getSchemaVersion() + "'"));
}

PipelineDefinition pipelineDefinition = config.getPipeline();
if (pipelineDefinition == null) {
errors.addError(Error.builder()
.withMessage("Missing pipeline configuration")
.withLocation(location("pipeline"))
);
} else {
if (pipelineDefinition.getApplication() == null) {
errors.addError(Error.builder()
.withMessage("Missing 'application' pipeline configuration")
.withLocation(location("pipeline.application"))
);
}
}

V1SchemaValidationHelper.validateStageDefinitions(config.getStages(), errors, V1TemplateConfigurationSchemaValidator::location);

// TODO rz - validate required variables are set and of the correct type
}

private static String location(String location) {
return "configuration:" + location;
}
}
@@ -0,0 +1,48 @@
/*
* 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.pipelinetemplate.v1schema.validator;

import com.netflix.spinnaker.orca.pipelinetemplate.v1schema.model.PipelineTemplate;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.Errors.Error;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.SchemaValidator;
import com.netflix.spinnaker.orca.pipelinetemplate.validator.VersionedSchema;

public class V1TemplateSchemaValidator implements SchemaValidator {

private static final String SUPPORTED_VERSION = "1";

@Override
public void validate(VersionedSchema pipelineTemplate, Errors errors) {
if (!(pipelineTemplate instanceof PipelineTemplate)) {
throw new IllegalArgumentException("Expected PipelineTemplate");
}
PipelineTemplate template = (PipelineTemplate) pipelineTemplate;

if (!SUPPORTED_VERSION.equals(template.getSchemaVersion())) {
errors.addError(Error.builder()
.withMessage("template schema version is unsupported: expected '" + SUPPORTED_VERSION + "', got '" + template.getSchemaVersion() + "'"));
}

V1SchemaValidationHelper.validateStageDefinitions(template.getStages(), errors, V1TemplateSchemaValidator::location);

// TODO rz - validate variable type & defaultValue combinations
}

private static String location(String location) {
return "template:" + location;
}
}

0 comments on commit 4c0082d

Please sign in to comment.