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

5
    include Gitlab::Ci::Config::Node::LegacyValidationHelpers
6

D
Douwe Maan 已提交
7
    DEFAULT_STAGE = 'test'
8
    ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
9 10
    ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
                        :allow_failure, :type, :stage, :when, :artifacts, :cache,
11 12
                        :dependencies, :before_script, :after_script, :variables,
                        :environment]
K
Kamil Trzcinski 已提交
13
    ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
14
    ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
D
Douwe Maan 已提交
15

16
    attr_reader :path, :cache, :stages
D
Douwe Maan 已提交
17

18
    def initialize(config, path = nil)
19 20 21
      @ci_config = Gitlab::Ci::Config.new(config)
      @config = @ci_config.to_hash

22
      @path = path
D
Douwe Maan 已提交
23

24 25 26
      unless @ci_config.valid?
        raise ValidationError, @ci_config.errors.first
      end
D
Douwe Maan 已提交
27

28
      initial_parsing
D
Douwe Maan 已提交
29
      validate!
30
    rescue Gitlab::Ci::Config::Loader::FormatError => e
31
      raise ValidationError, e.message
D
Douwe Maan 已提交
32 33
    end

34 35 36
    def jobs_for_ref(ref, tag = false, trigger_request = nil)
      @jobs.select do |_, job|
        process?(job[:only], job[:except], ref, tag, trigger_request)
37
      end
D
Douwe Maan 已提交
38 39
    end

40 41 42
    def jobs_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
      jobs_for_ref(ref, tag, trigger_request).select do |_, job|
        job[:stage] == stage
D
Douwe Maan 已提交
43 44 45
      end
    end

46 47
    def builds_for_ref(ref, tag = false, trigger_request = nil)
      jobs_for_ref(ref, tag, trigger_request).map do |name, job|
48
        build_attributes(name, _)
49
      end
50 51
    end

52
    def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
53 54
      jobs_for_stage_and_ref(stage, ref, tag, trigger_request).map do |name, _|
        build_attributes(name)
55 56
      end
    end
G
Grzegorz Bizon 已提交
57

58
    def builds
59 60
      @jobs.map do |name, _|
        build_attributes(name)
61
      end
62 63
    end

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
    def build_attributes(name)
      job = @jobs[name.to_sym] || {}
      {
        stage_idx: @stages.index(job[:stage]),
        stage: job[:stage],
        ##
        # Refactoring note:
        #  - before script behaves differently than after script
        #  - after script returns an array of commands
        #  - before script should be a concatenated command
        commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
        tag_list: job[:tags] || [],
        name: name,
        allow_failure: job[:allow_failure] || false,
        when: job[:when] || 'on_success',
        environment: job[:environment],
        yaml_variables: yaml_variables(name),
        options: {
          image: job[:image] || @image,
          services: job[:services] || @services,
          artifacts: job[:artifacts],
          cache: job[:cache] || @cache,
          dependencies: job[:dependencies],
          after_script: job[:after_script] || @after_script,
        }.compact
      }
    end

D
Douwe Maan 已提交
92 93 94
    private

    def initial_parsing
95 96
      @before_script = @ci_config.before_script
      @image = @ci_config.image
97
      @after_script = @ci_config.after_script
98
      @services = @ci_config.services
99
      @variables = @ci_config.variables
100
      @stages = @ci_config.stages
101
      @cache = @ci_config.cache
102

103
      @jobs = {}
D
Douwe Maan 已提交
104

105
      @config.except!(*ALLOWED_YAML_KEYS)
T
Tomasz Maczukin 已提交
106
      @config.each { |name, param| add_job(name, param) }
D
Douwe Maan 已提交
107

108
      raise ValidationError, "Please define at least one job" if @jobs.none?
D
Douwe Maan 已提交
109 110
    end

111 112 113 114 115 116 117
    def add_job(name, job)
      return if name.to_s.start_with?('.')

      raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script)

      stage = job[:stage] || job[:type] || DEFAULT_STAGE
      @jobs[name] = { stage: stage }.merge(job)
118 119
    end

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    def yaml_variables(name)
      variables = global_variables.merge(job_variables(name))
      variables.map do |key, value|
        { key: key, value: value, public: true }
      end
    end

    def global_variables
      @variables || {}
    end

    def job_variables(name)
      job = @jobs[name.to_sym]
      return {} unless job

      job[:variables] || {}
    end

D
Douwe Maan 已提交
138
    def validate!
K
Kamil Trzcinski 已提交
139 140 141 142 143 144 145
      @jobs.each do |name, job|
        validate_job!(name, job)
      end

      true
    end

D
Douwe Maan 已提交
146
    def validate_job!(name, job)
147 148 149
      validate_job_name!(name)
      validate_job_keys!(name, job)
      validate_job_types!(name, job)
K
Kamil Trzcinski 已提交
150
      validate_job_script!(name, job)
151 152

      validate_job_stage!(name, job) if job[:stage]
153
      validate_job_variables!(name, job) if job[:variables]
154 155
      validate_job_cache!(name, job) if job[:cache]
      validate_job_artifacts!(name, job) if job[:artifacts]
156
      validate_job_dependencies!(name, job) if job[:dependencies]
157 158 159
    end

    def validate_job_name!(name)
K
Kamil Trzcinski 已提交
160 161 162
      if name.blank? || !validate_string(name)
        raise ValidationError, "job name should be non-empty string"
      end
163
    end
K
Kamil Trzcinski 已提交
164

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

173
    def validate_job_types!(name, job)
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
      end

186 187
      if job[:only] && !validate_array_of_strings_or_regexps(job[:only])
        raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps"
D
Douwe Maan 已提交
188 189
      end

190 191
      if job[:except] && !validate_array_of_strings_or_regexps(job[:except])
        raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps"
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
      if job[:when] && !job[:when].in?(%w[on_success on_failure always manual])
        raise ValidationError, "#{name} job: when parameter should be on_success, on_failure, always or manual"
200
      end
201

202 203
      if job[:environment] && !validate_environment(job[:environment])
        raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}"
204
      end
205
    end
206

K
Kamil Trzcinski 已提交
207 208 209 210 211 212 213 214 215 216 217 218 219 220
    def validate_job_script!(name, job)
      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"
      end

      if job[:before_script] && !validate_array_of_strings(job[:before_script])
        raise ValidationError, "#{name} job: before_script should be an array of strings"
      end

      if job[:after_script] && !validate_array_of_strings(job[:after_script])
        raise ValidationError, "#{name} job: after_script should be an array of strings"
      end
    end

221
    def validate_job_stage!(name, job)
222 223
      unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
        raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
K
Kamil Trzcinski 已提交
224
      end
225
    end
K
Kamil Trzcinski 已提交
226

227
    def validate_job_variables!(name, job)
G
Grzegorz Bizon 已提交
228
      unless validate_variables(job[:variables])
229
        raise ValidationError,
G
Grzegorz Bizon 已提交
230
          "#{name} job: variables should be a map of key-value strings"
231 232 233
      end
    end

234
    def validate_job_cache!(name, job)
K
Kamil Trzcinski 已提交
235 236 237 238 239 240
      job[:cache].keys.each do |key|
        unless ALLOWED_CACHE_KEYS.include? key
          raise ValidationError, "#{name} job: cache unknown parameter #{key}"
        end
      end

241 242 243 244
      if job[:cache][:key] && !validate_string(job[:cache][:key])
        raise ValidationError, "#{name} job: cache:key parameter should be a string"
      end

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

249 250
      if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths])
        raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings"
251
      end
D
Douwe Maan 已提交
252 253
    end

254
    def validate_job_artifacts!(name, job)
K
Kamil Trzcinski 已提交
255 256 257 258 259 260
      job[:artifacts].keys.each do |key|
        unless ALLOWED_ARTIFACTS_KEYS.include? key
          raise ValidationError, "#{name} job: artifacts unknown parameter #{key}"
        end
      end

261 262 263 264
      if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
        raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
      end

265 266 267 268 269 270 271
      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
K
Kamil Trzcinski 已提交
272

K
Kamil Trzcinski 已提交
273
      if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always])
K
Kamil Trzcinski 已提交
274 275
        raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always"
      end
276 277 278 279

      if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in])
        raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration"
      end
280
    end
D
Douwe Maan 已提交
281

282
    def validate_job_dependencies!(name, job)
283
      unless validate_array_of_strings(job[:dependencies])
284 285 286
        raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
      end

287
      stage_index = @stages.index(job[:stage])
288 289

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

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

298
    def process?(only_params, except_params, ref, tag, trigger_request)
299
      if only_params.present?
300
        return false unless matching?(only_params, ref, tag, trigger_request)
301 302 303
      end

      if except_params.present?
304
        return false if matching?(except_params, ref, tag, trigger_request)
305 306 307 308 309
      end

      true
    end

310
    def matching?(patterns, ref, tag, trigger_request)
311
      patterns.any? do |pattern|
312
        match_ref?(pattern, ref, tag, trigger_request)
313 314 315
      end
    end

J
Jason Roehm 已提交
316
    def match_ref?(pattern, ref, tag, trigger_request)
317 318 319 320
      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 已提交
321
      return true if trigger_request.present? && pattern == 'triggers'
322 323 324 325 326 327 328

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