gitlab_ci_yaml_processor.rb 8.7 KB
Newer Older
D
Douwe Maan 已提交
1 2 3 4 5 6
module Ci
  class GitlabCiYamlProcessor
    class ValidationError < StandardError;end

    DEFAULT_STAGES = %w(build test deploy)
    DEFAULT_STAGE = 'test'
7 8
    ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
    ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
D
Douwe Maan 已提交
9

10
    attr_reader :before_script, :image, :services, :variables, :path, :cache
D
Douwe Maan 已提交
11

12
    def initialize(config, path = nil)
13
      @config = YAML.safe_load(config, [Symbol])
14
      @path = path
D
Douwe Maan 已提交
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48

      unless @config.is_a? Hash
        raise ValidationError, "YAML should be a hash"
      end

      @config = @config.deep_symbolize_keys

      initial_parsing

      validate!
    end

    def builds_for_stage_and_ref(stage, ref, tag = false)
      builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
    end

    def builds
      @jobs.map do |name, job|
        build_job(name, job)
      end
    end

    def stages
      @stages || DEFAULT_STAGES
    end

    private

    def initial_parsing
      @before_script = @config[:before_script] || []
      @image = @config[:image]
      @services = @config[:services]
      @stages = @config[:stages] || @config[:types]
      @variables = @config[:variables] || {}
49
      @cache = @config[:cache]
D
Douwe Maan 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62
      @config.except!(*ALLOWED_YAML_KEYS)

      # anything that doesn't have script is considered as unknown
      @config.each do |name, param|
        raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script)
      end

      unless @config.values.any?{|job| job.is_a?(Hash)}
        raise ValidationError, "Please define at least one job"
      end

      @jobs = {}
      @config.each do |key, job|
63
        next if key.to_s.start_with?('.')
D
Douwe Maan 已提交
64 65 66 67 68 69 70
        stage = job[:stage] || job[:type] || DEFAULT_STAGE
        @jobs[key] = { stage: stage }.merge(job)
      end
    end

    def build_job(name, job)
      {
K
Kamil Trzcinski 已提交
71
        stage_idx: stages.index(job[:stage]),
D
Douwe Maan 已提交
72
        stage: job[:stage],
K
Kamil Trzcinski 已提交
73 74
        commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
        tag_list: job[:tags] || [],
D
Douwe Maan 已提交
75 76 77 78
        name: name,
        only: job[:only],
        except: job[:except],
        allow_failure: job[:allow_failure] || false,
79
        when: job[:when] || 'on_success',
D
Douwe Maan 已提交
80 81
        options: {
          image: job[:image] || @image,
K
Kamil Trzcinski 已提交
82
          services: job[:services] || @services,
83 84
          artifacts: job[:artifacts],
          cache: job[:cache] || @cache,
85
          dependencies: job[:dependencies],
D
Douwe Maan 已提交
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
        }.compact
      }
    end

    def normalize_script(script)
      if script.is_a? Array
        script.join("\n")
      else
        script
      end
    end

    def validate!
      unless validate_array_of_strings(@before_script)
        raise ValidationError, "before_script should be an array of strings"
      end

      unless @image.nil? || @image.is_a?(String)
        raise ValidationError, "image should be a string"
      end

      unless @services.nil? || validate_array_of_strings(@services)
        raise ValidationError, "services should be an array of strings"
      end

      unless @stages.nil? || validate_array_of_strings(@stages)
        raise ValidationError, "stages should be an array of strings"
      end

      unless @variables.nil? || validate_variables(@variables)
        raise ValidationError, "variables should be a map of key-valued strings"
      end

119
      if @cache
120 121 122 123
        if @cache[:key] && !validate_string(@cache[:key])
          raise ValidationError, "cache:key parameter should be a string"
        end

124 125 126 127 128 129 130 131 132
        if @cache[:untracked] && !validate_boolean(@cache[:untracked])
          raise ValidationError, "cache:untracked parameter should be an boolean"
        end

        if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
          raise ValidationError, "cache:paths parameter should be an array of strings"
        end
      end

D
Douwe Maan 已提交
133
      @jobs.each do |name, job|
K
Kamil Trzcinski 已提交
134
        validate_job!(name, job)
D
Douwe Maan 已提交
135 136 137 138 139 140
      end

      true
    end

    def validate_job!(name, job)
141 142 143 144 145 146 147 148 149 150 151 152
      validate_job_name!(name)
      validate_job_keys!(name, job)
      validate_job_types!(name, job)

      validate_job_stage!(name, job) if job[:stage]
      validate_job_cache!(name, job) if job[:cache]
      validate_job_artifacts!(name, job) if job[:artifacts]
    end

    private

    def validate_job_name!(name)
K
Kamil Trzcinski 已提交
153 154 155
      if name.blank? || !validate_string(name)
        raise ValidationError, "job name should be non-empty string"
      end
156
    end
K
Kamil Trzcinski 已提交
157

158
    def validate_job_keys!(name, job)
D
Douwe Maan 已提交
159 160
      job.keys.each do |key|
        unless ALLOWED_JOB_KEYS.include? key
K
Kamil Trzcinski 已提交
161
          raise ValidationError, "#{name} job: unknown parameter #{key}"
D
Douwe Maan 已提交
162 163
        end
      end
164
    end
D
Douwe Maan 已提交
165

166
    def validate_job_types!(name, job)
K
Kamil Trzcinski 已提交
167 168
      if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
        raise ValidationError, "#{name} job: script should be a string or an array of a strings"
D
Douwe Maan 已提交
169 170
      end

K
Kamil Trzcinski 已提交
171 172
      if job[:image] && !validate_string(job[:image])
        raise ValidationError, "#{name} job: image should be a string"
D
Douwe Maan 已提交
173 174 175
      end

      if job[:services] && !validate_array_of_strings(job[:services])
K
Kamil Trzcinski 已提交
176
        raise ValidationError, "#{name} job: services should be an array of strings"
D
Douwe Maan 已提交
177 178 179
      end

      if job[:tags] && !validate_array_of_strings(job[:tags])
K
Kamil Trzcinski 已提交
180
        raise ValidationError, "#{name} job: tags parameter should be an array of strings"
D
Douwe Maan 已提交
181 182 183
      end

      if job[:only] && !validate_array_of_strings(job[:only])
K
Kamil Trzcinski 已提交
184
        raise ValidationError, "#{name} job: only parameter should be an array of strings"
D
Douwe Maan 已提交
185 186 187
      end

      if job[:except] && !validate_array_of_strings(job[:except])
K
Kamil Trzcinski 已提交
188
        raise ValidationError, "#{name} job: except parameter should be an array of strings"
D
Douwe Maan 已提交
189 190
      end

191 192
      if job[:allow_failure] && !validate_boolean(job[:allow_failure])
        raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
193 194
      end

195 196 197 198
      if job[:when] && !job[:when].in?(%w(on_success on_failure always))
        raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
      end
    end
199

200 201 202
    def validate_job_stage!(name, job)
      unless job[:stage].is_a?(String) && job[:stage].in?(stages)
        raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
K
Kamil Trzcinski 已提交
203
      end
204
    end
K
Kamil Trzcinski 已提交
205

206
    def validate_job_cache!(name, job)
207 208 209 210
      if job[:cache][:key] && !validate_string(job[:cache][:key])
        raise ValidationError, "#{name} job: cache:key parameter should be a string"
      end

211 212
      if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked])
        raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean"
D
Douwe Maan 已提交
213
      end
214

215 216
      if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths])
        raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings"
217
      end
D
Douwe Maan 已提交
218 219
    end

220
    def validate_job_artifacts!(name, job)
221 222 223 224
      if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
        raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
      end

225 226 227 228 229 230 231 232
      if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
        raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
      end

      if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths])
        raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings"
      end
    end
D
Douwe Maan 已提交
233 234

    def validate_array_of_strings(values)
K
Kamil Trzcinski 已提交
235
      values.is_a?(Array) && values.all? { |value| validate_string(value) }
D
Douwe Maan 已提交
236 237 238
    end

    def validate_variables(variables)
K
Kamil Trzcinski 已提交
239 240 241 242 243
      variables.is_a?(Hash) && variables.all? { |key, value| validate_string(key) && validate_string(value) }
    end

    def validate_string(value)
      value.is_a?(String) || value.is_a?(Symbol)
D
Douwe Maan 已提交
244
    end
245

246 247 248 249
    def validate_boolean(value)
      value.in?([true, false])
    end

250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    def process?(only_params, except_params, ref, tag)
      if only_params.present?
        return false unless matching?(only_params, ref, tag)
      end

      if except_params.present?
        return false if matching?(except_params, ref, tag)
      end

      true
    end

    def matching?(patterns, ref, tag)
      patterns.any? do |pattern|
        match_ref?(pattern, ref, tag)
      end
    end

    def match_ref?(pattern, ref, tag)
      pattern, path = pattern.split('@', 2)
      return false if path && path != self.path
      return true if tag && pattern == 'tags'
      return true if !tag && pattern == 'branches'

      if pattern.first == "/" && pattern.last == "/"
        Regexp.new(pattern[1...-1]) =~ ref
      else
        pattern == ref
      end
    end
D
Douwe Maan 已提交
280
  end
281
end