yaml_processor.rb 7.19 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7
module Gitlab
  module Ci
    class YamlProcessor
      ValidationError = Class.new(StandardError)

8
      include Gitlab::Config::Entry::LegacyValidationHelpers
9

10
      attr_reader :stages, :jobs
11

12 13 14 15 16 17
      ResultWithErrors = Struct.new(:content, :errors) do
        def valid?
          errors.empty?
        end
      end

18
      def initialize(config, opts = {})
19
        @ci_config = Gitlab::Ci::Config.new(config, **opts)
20 21 22 23 24 25 26
        @config = @ci_config.to_hash

        unless @ci_config.valid?
          raise ValidationError, @ci_config.errors.first
        end

        initial_parsing
27
      rescue Gitlab::Ci::Config::ConfigError => e
28 29 30
        raise ValidationError, e.message
      end

31 32 33 34 35 36 37 38 39 40 41 42
      def self.new_with_validation_errors(content, opts = {})
        return ResultWithErrors.new('', ['Please provide content of .gitlab-ci.yml']) if content.blank?

        config = Gitlab::Ci::Config.new(content, **opts)
        return ResultWithErrors.new("", config.errors) unless config.valid?

        config = Gitlab::Ci::YamlProcessor.new(content, opts)
        ResultWithErrors.new(config, [])
      rescue ValidationError, Gitlab::Ci::Config::ConfigError => e
        ResultWithErrors.new('', [e.message])
      end

43 44 45 46 47 48 49
      def builds
        @jobs.map do |name, _|
          build_attributes(name)
        end
      end

      def build_attributes(name)
50
        job = @jobs.fetch(name.to_sym, {})
51 52 53

        { stage_idx: @stages.index(job[:stage]),
          stage: job[:stage],
54
          tag_list: job[:tags],
55 56 57 58 59
          name: job[:name].to_s,
          allow_failure: job[:ignore],
          when: job[:when] || 'on_success',
          environment: job[:environment_name],
          coverage_regex: job[:coverage],
60
          yaml_variables: transform_to_yaml_variables(job_variables(name)),
61
          needs_attributes: job.dig(:needs, :job),
62
          interruptible: job[:interruptible],
63 64
          only: job[:only],
          except: job[:except],
65
          rules: job[:rules],
66
          cache: job[:cache],
67
          resource_group_key: job[:resource_group],
68
          scheduling_type: job[:scheduling_type],
69 70 71 72 73
          options: {
            image: job[:image],
            services: job[:services],
            artifacts: job[:artifacts],
            dependencies: job[:dependencies],
74
            cross_dependencies: job.dig(:needs, :cross_dependency),
75
            job_timeout: job[:timeout],
76 77 78 79
            before_script: job[:before_script],
            script: job[:script],
            after_script: job[:after_script],
            environment: job[:environment],
80
            retry: job[:retry],
81
            parallel: job[:parallel],
82
            instance: job[:instance],
83
            start_in: job[:start_in],
Matija Čupić's avatar
Matija Čupić committed
84
            trigger: job[:trigger],
85 86
            bridge_needs: job.dig(:needs, :bridge)&.first,
            release: release(job)
87
          }.compact }.compact
88 89
      end

90 91 92 93
      def release(job)
        job[:release] if Feature.enabled?(:ci_release_generation, default_enabled: false)
      end

94 95 96 97
      def stage_builds_attributes(stage)
        @jobs.values
          .select { |job| job[:stage] == stage }
          .map { |job| build_attributes(job[:name]) }
98 99
      end

100 101
      def stages_attributes
        @stages.uniq.map do |stage|
102
          seeds = stage_builds_attributes(stage)
103

104
          { name: stage, index: @stages.index(stage), builds: seeds }
105 106 107
        end
      end

108 109 110 111 112 113 114
      def workflow_attributes
        {
          rules: @config.dig(:workflow, :rules),
          yaml_variables: transform_to_yaml_variables(@variables)
        }
      end

115
      def self.validation_message(content, opts = {})
116 117 118
        return 'Please provide content of .gitlab-ci.yml' if content.blank?

        begin
119
          Gitlab::Ci::YamlProcessor.new(content, opts)
120
          nil
121
        rescue ValidationError => e
122 123
          e.message
        end
124 125
      end

126 127
      private

128 129 130 131 132 133 134 135 136 137
      def initial_parsing
        ##
        # Global config
        #
        @variables = @ci_config.variables
        @stages = @ci_config.stages

        ##
        # Jobs
        #
138
        @jobs = Ci::Config::Normalizer.new(@ci_config.jobs).normalize_jobs
139 140 141 142 143

        @jobs.each do |name, job|
          # logical validation for job
          validate_job_stage!(name, job)
          validate_job_dependencies!(name, job)
Kamil Trzciński's avatar
Kamil Trzciński committed
144
          validate_job_needs!(name, job)
145 146 147 148
          validate_job_environment!(name, job)
        end
      end

149 150
      def job_variables(name)
        job_variables = @jobs.dig(name.to_sym, :variables)
151

152 153
        @variables.to_h
          .merge(job_variables.to_h)
154 155
      end

156 157 158 159
      def transform_to_yaml_variables(variables)
        variables.to_h.map do |key, value|
          { key: key.to_s, value: value, public: true }
        end
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
      end

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

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

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

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

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

178 179 180
          dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage])

          unless dependency_stage_index.present? && dependency_stage_index < stage_index
181 182 183 184 185
            raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
          end
        end
      end

Kamil Trzciński's avatar
Kamil Trzciński committed
186
      def validate_job_needs!(name, job)
187
        return unless job.dig(:needs, :job)
Kamil Trzciński's avatar
Kamil Trzciński committed
188 189 190

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

191 192
        job.dig(:needs, :job).each do |need|
          need_job_name = need[:name]
Kamil Trzciński's avatar
Kamil Trzciński committed
193

194 195 196
          raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]

          needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage])
Kamil Trzciński's avatar
Kamil Trzciński committed
197 198

          unless needs_stage_index.present? && needs_stage_index < stage_index
199
            raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages"
Kamil Trzciński's avatar
Kamil Trzciński committed
200 201 202 203
          end
        end
      end

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
      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
    end
  end
end