manheim/cf_deployer

View on GitHub
lib/cf_deployer/config_validation.rb

Summary

Maintainability
B
5 hrs
Test Coverage
module CfDeployer
  class ConfigValidation

    class ValidationError < ApplicationError
    end

    CommonInputs = [:application, :environment, :component, :region]
    EnvironmentOptions = [:settings, :inputs, :tags, :components]
    ComponentOptions = [:settings, :inputs, :tags, :'depends-on', :'deployment-strategy', :'before-destroy', :'after-create', :'after-swap', :'after-update', :'defined_outputs', :'defined_parameters', :config_dir, :capabilities, :notify]

    def validate config, validate_inputs = true
      @config = config
      @errors = []
      @warnings = []
      check_application_name
      check_components validate_inputs
      check_environments
      @warnings.each { |message| puts "WARNNING:#{message}" }
      raise ValidationError.new(@errors.join("\n")) if @errors.length > 0
    end

    private

    def check_asg_name_output(component)
      component[:settings][:'auto-scaling-group-name-output'] ||= []
      outputs = component[:settings][:'auto-scaling-group-name-output'].map { |name| name.to_sym }
      missing_output_keys = (outputs - component[:defined_outputs].keys)
       @errors << "'#{missing_output_keys.map(&:to_s)}' is not a CF stack output" unless missing_output_keys.empty?
    end

    def check_cname_swap_options(component)
      @errors << "dns-fqdn is required when using cname-swap deployment-strategy" unless component[:settings][:'dns-fqdn']
      @errors << "dns-zone is required when using cname-swap deployment-strategy" unless component[:settings][:'dns-zone']
      @errors << "'#{component[:settings][:'elb-name-output']}' is not a CF stack output, which is required by cname-swap deployment" unless component[:defined_outputs].keys.include?(component[:settings][:'elb-name-output'].to_sym)
    end

    def check_application_name
      @config[:application] = "" unless @config[:application]
      return @errors << "Application name is missing in config" unless @config[:application].length > 0
      @errors << "Application name cannot be longer than 100 and can only contain letters, numbers, '-' and '.'" unless @config[:application] =~ /^[a-zA-Z0-9\.-]{1,100}$/
    end

    def check_components validate_inputs
      @config[:components] ||= {}
      return @errors << "At least one component must be defined in config" unless @config[:components].length > 0
      deployable_components = @config[:targets] || []
      component_targets = @config[:components].select { |key, value| deployable_components.include?(key.to_s) }
      invalid_names = deployable_components - component_targets.keys.map(&:to_s)
      @errors <<  "Found invalid deployment components #{invalid_names}" if invalid_names.size > 0
      component_targets.each do |component_name, component|
        component[:settings] ||= {}
        component[:inputs] ||= {}
        component[:defined_outputs] ||= {}
        @errors << "Component name cannot be longer than 100 and can only contain letters, numbers, '-' and '.': #{component_name}" unless component_name =~ /^[A-Za-z0-9\.-]{1,100}$/
        check_parameters component_name, component if validate_inputs
        check_cname_swap_options(component) if component[:'deployment-strategy'] == 'cname-swap'
        check_asg_name_output(component)
        check_hooks(component)
        check_component_options(component_name, component)
      end
    end

    def check_component_options(name, component)
      component.keys.each do |option|
        @errors << "The option '#{option}' of the component '#{name}' is not valid" unless ComponentOptions.include?(option)
      end
    end

    def check_hooks(component)
      hook_names = [:'before-destroy', :'after-create', :'after-swap']
      hook_names.each do |hook_name|
        next unless component[hook_name] && component[hook_name].is_a?(Hash)
        @errors << "Invalid hook '#{hook_name}'" unless component[hook_name][:file] || component[hook_name][:code]
        check_hook_file(component, hook_name)
      end
    end

    def check_hook_file(component, hook_name)
      file_name = component[hook_name][:file]
      return unless file_name
      path = File.join(component[:config_dir], file_name)
      @errors << "File '#{path}' does not exist, which is required by hook '#{hook_name}'" unless File.exists?(path)
    end

    def check_environments
      @config[:environments] ||= {}
      @config[:environments].each do | name, environment |
        @errors << "Environment name cannot be longer than 12 and can only contain letters, numbers, '-' and '.': #{name}" unless name =~ /^[a-zA-Z0-9\.-]{1,12}$/
        check_environment_options(name, environment)
      end
    end

    def check_environment_options(name, environment)
      environment.keys.each do |option|
        @errors << "The option '#{option}' of the environment '#{name}' is not valid" unless EnvironmentOptions.include?(option)
        end
    end


    def check_parameters(component_name, component)
      component[:defined_parameters] ||= {}
      component[:defined_outputs] ||= {}
      component[:defined_parameters].each do | parameter_name, parameter |
        if component[:inputs].keys.include?(parameter_name) || parameter[:Default]
          check_output_reference(parameter_name, component_name)
        else
          @errors << "No input setting '#{parameter_name}' found for CF template parameter in component #{component_name}"
        end
      end
      check_un_used_inputs(component_name, component)
    end

    def check_un_used_inputs(component_name, component)
      component[:inputs].keys.each do |input|
        unless component[:defined_parameters].keys.include?(input) || CommonInputs.include?(input)
          message = "The input '#{input}' defined in the component '#{component_name}' is not used in the json template as a parameter"
          if component[:settings][:'raise-error-for-unused-inputs']
            @errors << message
          else
            @warnings << message
          end
        end
      end
    end


    def check_output_reference(setting_name, component_name)
      setting = @config[:components][component_name][:inputs][setting_name]
      return unless setting.is_a?(Hash)
      ref_component_name = setting[:component].to_sym
      ref_component = @config[:components][ref_component_name]
      if ref_component
        output_key = setting[:'output-key'].to_sym
        @errors << "No output '#{output_key}' found in CF template of component #{ref_component_name}, which is referenced by input setting '#{setting_name}' in component #{component_name}" unless ref_component[:defined_outputs].keys.include?(output_key)
      else
        @errors << "No component '#{ref_component_name}' found in CF template, which is referenced by input setting '#{setting_name}' in component #{component_name}"
      end
    end

  end
end