summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/config/external/file/base.rb
blob: 65caf4ac47d5d9fd846161341a5aa1ee2efd3fd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# frozen_string_literal: true

module Gitlab
  module Ci
    class Config
      module External
        module File
          class Base
            include Gitlab::Utils::StrongMemoize

            attr_reader :location, :params, :context, :errors

            YAML_WHITELIST_EXTENSION = /.+\.(yml|yaml)$/i.freeze

            def initialize(params, context)
              @params = params
              @context = context
              @errors = []
            end

            def matching?
              location.present?
            end

            def invalid_location_type?
              !location.is_a?(String)
            end

            def invalid_extension?
              location.nil? || !::File.basename(location).match?(YAML_WHITELIST_EXTENSION)
            end

            def valid?
              errors.none?
            end

            def error_message
              errors.first
            end

            def content
              raise NotImplementedError, 'subclass must implement fetching raw content'
            end

            def to_hash
              expanded_content_hash
            end

            def validate!
              context.check_execution_time! if ::Feature.disabled?(:ci_refactoring_external_mapper, context.project)
              validate_location!
              validate_context! if valid?
              fetch_and_validate_content! if valid?
              load_and_validate_expanded_hash! if valid?
            end

            def metadata
              {
                context_project: context.project&.full_path,
                context_sha: context.sha
              }
            end

            def eql?(other)
              other.hash == hash
            end

            def hash
              [params, context.project&.full_path, context.sha].hash
            end

            protected

            def expanded_content_hash
              return unless content_hash

              strong_memoize(:expanded_content_yaml) do
                expand_includes(content_hash)
              end
            end

            def content_hash
              strong_memoize(:content_yaml) do
                ::Gitlab::Ci::Config::Yaml.load!(content)
              end
            rescue Gitlab::Config::Loader::FormatError
              nil
            end

            def validate_location!
              if invalid_location_type?
                errors.push("Included file `#{masked_location}` needs to be a string")
              elsif invalid_extension?
                errors.push("Included file `#{masked_location}` does not have YAML extension!")
              end
            end

            def validate_context!
              raise NotImplementedError, 'subclass must implement validate_context'
            end

            def fetch_and_validate_content!
              context.logger.instrument(:config_file_fetch_content) do
                content # calling the method fetches then memoizes the result
              end

              return if errors.any?

              context.logger.instrument(:config_file_validate_content) do
                validate_content!
              end
            end

            def load_and_validate_expanded_hash!
              context.logger.instrument(:config_file_fetch_content_hash) do
                content_hash # calling the method loads then memoizes the result
              end

              context.logger.instrument(:config_file_expand_content_includes) do
                expanded_content_hash # calling the method expands then memoizes the result
              end

              validate_hash!
            end

            def validate_content!
              if content.blank?
                errors.push("Included file `#{masked_location}` is empty or does not exist!")
              end
            end

            def validate_hash!
              if to_hash.blank?
                errors.push("Included file `#{masked_location}` does not have valid YAML syntax!")
              end
            end

            def expand_includes(hash)
              External::Processor.new(hash, context.mutate(expand_context_attrs)).perform
            end

            def expand_context_attrs
              {}
            end

            def masked_location
              strong_memoize(:masked_location) do
                context.mask_variables_from(location)
              end
            end
          end
        end
      end
    end
  end
end