All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jkube.kit.common.util.validator.ResourceValidator Maven / Gradle / Ivy

There is a newer version: 1.16.2
Show newest version
/**
 * Copyright (c) 2019 Red Hat, Inc.
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at:
 *
 *     https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.jkube.kit.common.util.validator;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Path;
import javax.validation.metadata.ConstraintDescriptor;

import org.eclipse.jkube.kit.common.KitLogger;
import org.eclipse.jkube.kit.common.util.ResourceClassifier;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.networknt.schema.JsonMetaSchema;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.NonValidationKeyword;
import com.networknt.schema.ValidationMessage;

/**
 * Validates Kubernetes/OpenShift resource descriptors using JSON schema validation method.
 * For OpenShift, it adds some exceptions from JSON schema constraints and ignores some validation errors.
 */

public class ResourceValidator {

    public static final String SCHEMA_JSON = "schema/validation-schema.json";
    private KitLogger log;
    private File[] resources;
    private ResourceClassifier target = ResourceClassifier.KUBERNETES;
    private List ignoreValidationRules = new ArrayList<>();

    /**
     * @param inputFile File/Directory path of resource descriptors
     */
    public ResourceValidator(File inputFile) {
        if(inputFile.isDirectory()) {
            resources = inputFile.listFiles();
        } else {
            resources = new File[]{inputFile};
        }
    }

    /**
     * @param inputFile File/Directory path of resource descriptors
     * @param target  Target platform e.g OpenShift, Kubernetes
     * @param log KitLogger for logging messages on standard output devices
     */
    public ResourceValidator(File inputFile, ResourceClassifier target, KitLogger log) {
        this(inputFile);
        this.target = target;
        this.log = log;
        setupIgnoreRules(this.target);
    }

    /*
     * Add exception rules to ignore validation constraint from JSON schema for OpenShift/Kubernetes resources. Some fields in JSON schema which are marked as required
     * but in reality it's not required to provide values for those fields while creating the resources.
     * e.g. In DeploymentConfig(https://docs.openshift.com/container-platform/3.6/rest_api/openshift_v1.html#v1-deploymentconfig) model 'status' field is marked as
     * required.
     */
    private void setupIgnoreRules(ResourceClassifier target) {
        ignoreValidationRules.add(new IgnorePortValidationRule(ValidationRule.TYPE));
        ignoreValidationRules.add(new IgnoreResourceMemoryLimitRule(ValidationRule.TYPE));
    }



    /**
     * Validates the resource descriptors as per JSON schema. If any resource is invalid it throws @{@link ConstraintViolationException} with
     * all violated constraints
     *
     * @return number of resources processed
     * @throws ConstraintViolationException  ConstraintViolationException
     * @throws IOException IOException
     */
    public int validate() throws IOException {
        for(File resource: resources) {
            if (resource.isFile() && resource.exists()) {
                log.info("validating %s resource", resource.toString());
                JsonNode inputSpecNode = geFileContent(resource);
                String kind = inputSpecNode.get("kind").toString();
                for (URL schemaFile : Collections.list(ResourceValidator.class.getClassLoader().getResources(SCHEMA_JSON))) {
                    JsonSchema schema = getJsonSchema(schemaFile, kind);
                    Set errors = schema.validate(inputSpecNode);
                    processErrors(errors, resource);
                }
            }
        }

        return resources.length;
    }

    private void processErrors(Set errors, File resource) {
        Set constraintViolations = new HashSet<>();
        for (ValidationMessage errorMsg: errors) {
            if(!ignoreError(errorMsg))
                constraintViolations.add(new ConstraintViolationImpl(errorMsg));
        }

        if(!constraintViolations.isEmpty()) {
            throw new ConstraintViolationException(getErrorMessage(resource, constraintViolations), constraintViolations);
        }
    }

    private boolean ignoreError(ValidationMessage errorMsg) {
        for (ValidationRule rule : ignoreValidationRules) {
            if(rule.ignore(errorMsg)) {
                return  true;
            }
        }

        return false;
    }

    private String getErrorMessage(File resource, Set violations) {
        StringBuilder validationError = new StringBuilder();
        validationError.append("Invalid Resource : ");
        validationError.append(resource.toString());

        for (ConstraintViolationImpl violation: violations) {
            validationError.append("\n");
            validationError.append(violation.toString());
        }

        return  validationError.toString();
    }

    private JsonSchema getJsonSchema(URL schemaUrl, String kind) throws IOException {
        final JsonMetaSchema v201909 = JsonMetaSchema.getV201909();
        final String defaultUri = v201909.getUri();
        JsonObject jsonSchema = fixUrlIfUnversioned(getSchemaJson(schemaUrl), defaultUri);
        checkIfKindPropertyExists(kind);
        getResourceProperties(kind, jsonSchema);
        final JsonMetaSchema metaSchema = JsonMetaSchema.builder(v201909.getUri(), v201909)
            .addKeyword(new NonValidationKeyword("javaType"))
            .addKeyword(new NonValidationKeyword("javaInterfaces"))
            .addKeyword(new NonValidationKeyword("resources"))
            .build();
        return new JsonSchemaFactory.Builder()
            .defaultMetaSchemaURI(defaultUri).addMetaSchema(metaSchema).build()
            .getSchema(jsonSchema.toString());
    }

    private void getResourceProperties(String kind, JsonObject jsonSchema) {
        String kindKey = kind.replaceAll("\"", "").toLowerCase();
        if (jsonSchema.get("resources") != null && jsonSchema.get("resources").getAsJsonObject().get(kindKey) != null) {
            jsonSchema.add("properties" , jsonSchema.get("resources").getAsJsonObject()
                    .getAsJsonObject(kindKey)
                    .getAsJsonObject("properties"));
        }
    }

    private void checkIfKindPropertyExists(String kind) {
        if(kind == null) {
            throw new JsonIOException("Invalid kind of resource or 'kind' is missing from resource definition");
        }
    }

    private JsonNode geFileContent(File file) throws IOException {
        try (InputStream resourceStream = new FileInputStream(file)) {
            ObjectMapper jsonMapper = new ObjectMapper(new YAMLFactory());
            return jsonMapper.readTree(resourceStream);
        }
    }

    public JsonObject getSchemaJson(URL schemaUrl) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String rootNode = objectMapper.readValue(schemaUrl, JsonNode.class).toString();
        JsonObject jsonObject = new JsonParser().parse(rootNode).getAsJsonObject();
        jsonObject.remove("id");
        return jsonObject;
    }

    private class ConstraintViolationImpl implements ConstraintViolation {

        private ValidationMessage errorMsg;

        public ConstraintViolationImpl(ValidationMessage errorMsg) {
            this.errorMsg = errorMsg;
        }

        @Override
        public String getMessage() {
            return errorMsg.getMessage();
        }

        @Override
        public String getMessageTemplate() {
            return null;
        }

        @Override
        public ValidationMessage getRootBean() {
            return null;
        }

        @Override
        public Class getRootBeanClass() {
            return null;
        }

        @Override
        public Object getLeafBean() {
            return null;
        }

        @Override
        public Object[] getExecutableParameters() {
            return new Object[0];
        }

        @Override
        public Object getExecutableReturnValue() {
            return null;
        }

        @Override
        public Path getPropertyPath() {
            return null;
        }

        @Override
        public Object getInvalidValue() {
            return null;
        }

        @Override
        public ConstraintDescriptor getConstraintDescriptor() {
            return null;
        }

        @Override
        public  U unwrap(Class aClass) {
            return null;
        }

        @Override
        public String toString() {
            return "[message=" + getMessage().replaceFirst("[$]", "") +", violation type="+errorMsg.getType()+"]";
        }
    }

    private static JsonObject fixUrlIfUnversioned(JsonObject jsonSchema, String versionedUri) {
        final String uri = jsonSchema.get("$schema").getAsString();
        if (uri.matches("^https?://json-schema.org/draft-05/schema[^/]*$")) {
            final JsonObject ret = jsonSchema.deepCopy();
            ret.addProperty("$schema", versionedUri);
            return ret;
        }
        return jsonSchema;
    }

}