BigW Consortium Gitlab

gitlab_ci_yaml_processor.rb 6.01 KB
Newer Older
1 2
module Ci
  class GitlabCiYamlProcessor
3
    class ValidationError < StandardError; end
4

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

7
    attr_reader :path, :cache, :stages, :jobs
8

9
    def initialize(config, path = nil)
10
      @ci_config = Gitlab::Ci::Config.new(config)
11 12
      @config = @ci_config.to_hash
      @path = path
13

14 15 16
      unless @ci_config.valid?
        raise ValidationError, @ci_config.errors.first
      end
17

18
      initial_parsing
19
    rescue Gitlab::Ci::Config::Loader::FormatError => e
20
      raise ValidationError, e.message
21 22
    end

23 24 25
    def jobs_for_ref(ref, tag = false, trigger_request = nil)
      @jobs.select do |_, job|
        process?(job[:only], job[:except], ref, tag, trigger_request)
26
      end
27 28
    end

29 30 31
    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
32 33 34
      end
    end

35
    def builds_for_ref(ref, tag = false, trigger_request = nil)
Kamil Trzcinski committed
36
      jobs_for_ref(ref, tag, trigger_request).map do |name, _|
37
        build_attributes(name)
38
      end
39 40
    end

41
    def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
42 43
      jobs_for_stage_and_ref(stage, ref, tag, trigger_request).map do |name, _|
        build_attributes(name)
44 45
      end
    end
46

47
    def builds
48 49
      @jobs.map do |name, _|
        build_attributes(name)
50
      end
51 52
    end

53 54 55 56 57
    def build_attributes(name)
      job = @jobs[name.to_sym] || {}
      {
        stage_idx: @stages.index(job[:stage]),
        stage: job[:stage],
58
        commands: job[:commands],
59
        tag_list: job[:tags] || [],
60
        name: job[:name].to_s,
61 62
        allow_failure: job[:allow_failure] || false,
        when: job[:when] || 'on_success',
63
        environment: job[:environment_name],
64 65
        yaml_variables: yaml_variables(name),
        options: {
66 67
          image: job[:image],
          services: job[:services],
68
          artifacts: job[:artifacts],
69
          cache: job[:cache],
70
          dependencies: job[:dependencies],
71
          after_script: job[:after_script],
72
          environment: job[:environment],
73 74
        }.compact
      }
75 76
    end

Katarzyna Kobierska committed
77
    def self.validation_message(content)
Katarzyna Kobierska committed
78
      return 'Please provide content of .gitlab-ci.yml' if content.blank?
Katarzyna Kobierska committed
79

Katarzyna Kobierska committed
80 81 82 83 84
      begin
        Ci::GitlabCiYamlProcessor.new(content)
        nil
      rescue ValidationError, Psych::SyntaxError => e
        e.message
85 86 87
      end
    end

88 89 90
    private

    def initial_parsing
91 92 93
      ##
      # Global config
      #
94 95
      @before_script = @ci_config.before_script
      @image = @ci_config.image
96
      @after_script = @ci_config.after_script
97
      @services = @ci_config.services
98
      @variables = @ci_config.variables
99
      @stages = @ci_config.stages
100
      @cache = @ci_config.cache
101

102 103 104 105
      ##
      # Jobs
      #
      @jobs = @ci_config.jobs
106 107

      @jobs.each do |name, job|
108 109 110 111
        # logical validation for job

        validate_job_stage!(name, job)
        validate_job_dependencies!(name, job)
112
        validate_job_environment!(name, job)
113
      end
114 115
    end

116
    def yaml_variables(name)
117 118 119
      variables = (@variables || {})
        .merge(job_variables(name))

120 121 122 123 124 125 126 127 128 129
      variables.map do |key, value|
        { key: key, value: value, public: true }
      end
    end

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

      job[:variables] || {}
130 131
    end

132
    def validate_job_stage!(name, job)
133 134
      return unless job[:stage]

135 136 137 138 139
      unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
        raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
      end
    end

140
    def validate_job_dependencies!(name, job)
141
      return unless job[:dependencies]
142

143
      stage_index = @stages.index(job[:stage])
144 145

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

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

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
    def validate_job_environment!(name, job)
      return unless job[:environment]
      return unless job[:environment].is_a?(Hash)

      environment = job[:environment]
      validate_on_stop_job!(name, environment, environment[:on_stop])
    end

    def validate_on_stop_job!(name, environment, on_stop)
      return unless on_stop

      on_stop_job = @jobs[on_stop.to_sym]
      unless on_stop_job
        raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
      end

      unless on_stop_job[:environment]
        raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
      end

      unless on_stop_job[:environment][:name] == environment[:name]
        raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
      end

      unless on_stop_job[:environment][:action] == 'stop'
        raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
      end
    end

183
    def process?(only_params, except_params, ref, tag, trigger_request)
184
      if only_params.present?
185
        return false unless matching?(only_params, ref, tag, trigger_request)
186 187 188
      end

      if except_params.present?
189
        return false if matching?(except_params, ref, tag, trigger_request)
190 191 192 193 194
      end

      true
    end

195
    def matching?(patterns, ref, tag, trigger_request)
196
      patterns.any? do |pattern|
197
        match_ref?(pattern, ref, tag, trigger_request)
198 199 200
      end
    end

201
    def match_ref?(pattern, ref, tag, trigger_request)
202 203 204 205
      pattern, path = pattern.split('@', 2)
      return false if path && path != self.path
      return true if tag && pattern == 'tags'
      return true if !tag && pattern == 'branches'
206
      return true if trigger_request.present? && pattern == 'triggers'
207 208 209 210 211 212 213

      if pattern.first == "/" && pattern.last == "/"
        Regexp.new(pattern[1...-1]) =~ ref
      else
        pattern == ref
      end
    end
214
  end
215
end