gitlab_ci_yaml_processor.rb 9.6 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
    ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
8 9 10
    ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
                        :allow_failure, :type, :stage, :when, :artifacts, :cache,
                        :dependencies]
D
Douwe Maan 已提交
11

12
    attr_reader :before_script, :image, :services, :variables, :path, :cache
D
Douwe Maan 已提交
13

14
    def initialize(config, path = nil)
15
      @config = YAML.safe_load(config, [Symbol], [], true)
16
      @path = path
D
Douwe Maan 已提交
17 18 19 20 21 22 23 24 25 26 27 28

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

      @config = @config.deep_symbolize_keys

      initial_parsing

      validate!
    end

29 30
    def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
      builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
D
Douwe Maan 已提交
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    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] || {}
51
      @cache = @config[:cache]
D
Douwe Maan 已提交
52 53 54 55 56 57 58 59 60 61 62 63 64
      @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|
65
        next if key.to_s.start_with?('.')
D
Douwe Maan 已提交
66 67 68 69 70 71 72
        stage = job[:stage] || job[:type] || DEFAULT_STAGE
        @jobs[key] = { stage: stage }.merge(job)
      end
    end

    def build_job(name, job)
      {
K
Kamil Trzcinski 已提交
73
        stage_idx: stages.index(job[:stage]),
D
Douwe Maan 已提交
74
        stage: job[:stage],
K
Kamil Trzcinski 已提交
75 76
        commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
        tag_list: job[:tags] || [],
D
Douwe Maan 已提交
77 78 79 80
        name: name,
        only: job[:only],
        except: job[:except],
        allow_failure: job[:allow_failure] || false,
81
        when: job[:when] || 'on_success',
D
Douwe Maan 已提交
82 83
        options: {
          image: job[:image] || @image,
K
Kamil Trzcinski 已提交
84
          services: job[:services] || @services,
85 86
          artifacts: job[:artifacts],
          cache: job[:cache] || @cache,
87
          dependencies: job[:dependencies],
D
Douwe Maan 已提交
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 119 120
        }.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

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

126 127 128 129 130 131 132 133 134
        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 已提交
135
      @jobs.each do |name, job|
K
Kamil Trzcinski 已提交
136
        validate_job!(name, job)
D
Douwe Maan 已提交
137 138 139 140 141 142
      end

      true
    end

    def validate_job!(name, job)
143 144 145 146 147 148 149
      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]
150
      validate_job_dependencies!(name, job) if job[:dependencies]
151 152 153 154 155
    end

    private

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

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

169
    def validate_job_types!(name, job)
K
Kamil Trzcinski 已提交
170 171
      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 已提交
172 173
      end

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

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

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

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

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

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

198 199 200 201
      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
202

203 204 205
    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 已提交
206
      end
207
    end
K
Kamil Trzcinski 已提交
208

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

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

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

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

228 229 230 231 232 233 234 235
      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 已提交
236

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
    def validate_job_dependencies!(name, job)
      if !validate_array_of_strings(job[:dependencies])
        raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
      end

      stage_index = stages.index(job[:stage])

      job[:dependencies].each do |dependency|
        raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]

        unless stages.index(@jobs[dependency][:stage]) < stage_index
          raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
        end
      end
    end

D
Douwe Maan 已提交
253
    def validate_array_of_strings(values)
K
Kamil Trzcinski 已提交
254
      values.is_a?(Array) && values.all? { |value| validate_string(value) }
D
Douwe Maan 已提交
255 256 257
    end

    def validate_variables(variables)
K
Kamil Trzcinski 已提交
258 259 260 261 262
      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 已提交
263
    end
264

265 266 267 268
    def validate_boolean(value)
      value.in?([true, false])
    end

269
    def process?(only_params, except_params, ref, tag, trigger_request)
270
      if only_params.present?
271
        return false unless matching?(only_params, ref, tag, trigger_request)
272 273 274
      end

      if except_params.present?
275
        return false if matching?(except_params, ref, tag, trigger_request)
276 277 278 279 280
      end

      true
    end

281
    def matching?(patterns, ref, tag, trigger_request)
282
      patterns.any? do |pattern|
283
        match_ref?(pattern, ref, tag, trigger_request)
284 285 286 287 288 289 290 291
      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'
J
Jason Roehm 已提交
292
      return true if !trigger_request.nil? && pattern == 'triggers'
293 294 295 296 297 298 299

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