This file is indexed.

/usr/lib/ruby/vendor_ruby/octocatalog-diff/util/catalogs.rb is in octocatalog-diff 1.5.3-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# frozen_string_literal: true

require 'json'
require 'open3'
require 'yaml'
require_relative '../catalog'
require_relative '../errors'
require_relative 'parallel'

module OctocatalogDiff
  module Util
    # Helper class to construct catalogs, performing all necessary steps such as
    # bootstrapping directories, installing facts, and running puppet.
    class Catalogs
      # Constructor
      # @param options [Hash] Options
      # @param logger [Logger] Logger object
      def initialize(options, logger)
        @options = options
        @logger = logger
        @catalogs = nil
        raise '@logger must not be nil' if @logger.nil?
      end

      # Compile catalogs. This handles building both the old and new catalog (in parallel) and returns
      # only when both catalogs have been built.
      # @return [Hash] { :from => [OctocatalogDiff::Catalog], :to => [OctocatalogDiff::Catalog] }
      def catalogs
        @catalogs ||= build_catalog_parallelizer
      end

      # Handles the "bootstrap then exit" option, which bootstraps directories but
      # exits without compiling catalogs.
      def bootstrap_then_exit
        @logger.debug('Begin bootstrap_then_exit')
        OctocatalogDiff::CatalogUtil::Bootstrap.bootstrap_directory_parallelizer(@options, @logger)
        @logger.debug('Success bootstrap_then_exit')
        @logger.info('Successfully completed --bootstrap-then-exit action')
      end

      private

      # Parallelizes bootstrapping of directories and building catalogs.
      # @return [Hash] { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }
      def build_catalog_parallelizer
        # Construct parallel tasks. The array supplied to OctocatalogDiff::Util::Parallel is the task portion
        # of each of the tuples in catalog_tasks.
        catalog_tasks = build_catalog_tasks

        # Update any tasks for catalogs that do not need to be compiled. This is the case when --catalog-only
        # is specified and only one catalog is to be built. This will change matching catalog tasks to the 'noop' type.
        catalog_tasks.map! do |x|
          if @options["#{x[0]}_catalog".to_sym] == '-'
            x[1].args[:backend] = :noop
          elsif @options["#{x[0]}_catalog".to_sym].is_a?(String)
            x[1].args[:json] = File.read(@options["#{x[0]}_catalog".to_sym])
            x[1].args[:backend] = :json
          end
          x
        end

        # Initialize the objects for each parallel task. Initializing the object is very fast and does not actually
        # build the catalog.
        result = {}
        catalog_tasks.each do |x|
          result[x[0]] = OctocatalogDiff::Catalog.create(x[1].args)
          @logger.debug "Initialized #{result[x[0]].builder} for #{x[0]}-catalog"
        end

        # Disable --compare-file-text if either (or both) of the chosen backends do not support it
        if @options.fetch(:compare_file_text, false)
          result.each do |_key, builder_obj|
            next if builder_obj.convert_file_resources(true)
            @logger.debug "Disabling --compare-file-text; not supported by #{builder_obj.builder}"
            @options[:compare_file_text] = false
            catalog_tasks.map! do |x|
              x[1].args[:compare_file_text] = false
              x
            end
            break
          end
        end

        # Inject the starting object into the catalog tasks
        catalog_tasks.map! do |x|
          x[1].args[:object] = result[x[0]]
          x
        end

        # Execute the parallelized catalog builds
        passed_catalog_tasks = catalog_tasks.map { |x| x[1] }
        parallel_catalogs = OctocatalogDiff::Util::Parallel.run_tasks(passed_catalog_tasks, @logger, @options[:parallel])

        # If the catalogs array is empty at this point, there is an unexpected size mismatch. This should
        # never happen, but test for it anyway.
        unless parallel_catalogs.size == catalog_tasks.size
          # :nocov:
          raise "BUG: mismatch catalog_result (#{parallel_catalogs.size} vs #{catalog_tasks.size})"
          # :nocov:
        end

        # If catalogs failed to compile, report that. Prefer to display an actual failure message rather
        # than a generic incomplete parallel task message if there is a more specific message present.
        failures = parallel_catalogs.reject(&:status)
        if failures.any?
          f = failures.reject { |r| r.exception.is_a?(OctocatalogDiff::Util::Parallel::IncompleteTask) }.first
          f ||= failures.first
          raise f.exception
        end

        # Construct result hash. Will eventually be in the format
        # { :from => OctocatalogDiff::Catalog, :to => OctocatalogDiff::Catalog }

        # Analyze the results from parallel run.
        catalog_tasks.each do |x|
          # The `parallel_catalog_obj` is a OctocatalogDiff::Util::Parallel::Result. Get the first element from
          # the parallel_catalogs output.
          parallel_catalog_obj = parallel_catalogs.shift

          # Add the result to the 'result' hash
          add_parallel_result(result, parallel_catalog_obj, x)
        end

        # Things have succeeded if the :to and :from catalogs exist at this point. If not, things have
        # failed, and an exception should be thrown.
        return result if result.key?(:to) && result.key?(:from)

        # This is believed to be a bug condition.
        # :nocov:
        raise OctocatalogDiff::Errors::CatalogError, 'One or more catalogs failed to compile.'
        # :nocov:
      end

      # Get catalog compilation tasks.
      # @return [Array<[key, task]>] Catalog tasks
      def build_catalog_tasks
        [:from, :to].map do |key|
          # These are arguments to OctocatalogDiff::Util::Parallel::Task. In most cases the arguments
          # of OctocatalogDiff::Util::Parallel::Task are taken directly from options, but there are
          # some defaults or otherwise-named options that must be set here.
          args = @options.merge(
            tag: key.to_s,
            branch: @options["#{key}_env".to_sym] || '-',
            bootstrapped_dir: @options["bootstrapped_#{key}_dir".to_sym],
            basedir: @options[:basedir],
            compare_file_text: @options.fetch(:compare_file_text, true),
            retry_failed_catalog: @options.fetch(:retry_failed_catalog, 0),
            parser: @options["parser_#{key}".to_sym]
          )
          args[:basedir] ||= args[:bootstrapped_dir]

          # If any options are in the form of 'to_SOMETHING' or 'from_SOMETHING', this sets the option to
          # 'SOMETHING' for the catalog if it matches this key. For example, when compiling the 'to' catalog
          # when an option of :to_some_arg => 'foo', this sets :some_arg => foo, and deletes :to_some_arg and
          # :from_some_arg.
          @options.keys.select { |x| x.to_s =~ /^(to|from)_/ }.each do |opt_key|
            args[opt_key.to_s.sub(/^(to|from)_/, '').to_sym] = @options[opt_key] if opt_key.to_s.start_with?(key.to_s)
            args.delete(opt_key)
          end

          # Skip reference validation in the from-catalog by saying we already performed it.
          args[:references_validated] = (key == :from)

          # The task is a OctocatalogDiff::Util::Parallel::Task object that contains the method to execute,
          # validator method, text description, and arguments to provide when calling the method.
          task = OctocatalogDiff::Util::Parallel::Task.new(
            method: method(:build_catalog),
            validator: method(:catalog_validator),
            validator_args: { task: key },
            description: "build_catalog for #{@options["#{key}_env".to_sym]}",
            args: args
          )

          # The format of `catalog_tasks` will be a tuple, where the first element is the key
          # (e.g. :to or :from) and the second element is the OctocatalogDiff::Util::Parallel::Task object.
          [key, task]
        end.compact
      end

      # Given a result from the 'parallel' run and a corresponding (key,task) tuple, add valid
      # catalogs to the 'result' hash and throw errors for invalid catalogs.
      # @param result [Hash] Result hash for build_catalog_parallelizer (may be modified)
      # @param parallel_catalog_obj [OctocatalogDiff::Util::Parallel::Result] Parallel catalog result
      # @param key_task_tuple [Array<key, task>] Key, task tuple
      def add_parallel_result(result, parallel_catalog_obj, key_task_tuple)
        # Expand the tuple into variables
        key, task = key_task_tuple

        # For reporting purposes, get the branch name.
        branch = task.args[:branch]

        # Check the result of the parallel run on this object.
        if parallel_catalog_obj.status.nil?
          # The compile was killed because another task failed.
          @logger.warn "Catalog compile for #{branch} was aborted due to another failure"

        elsif parallel_catalog_obj.output.is_a?(OctocatalogDiff::Catalog)
          # The result is a catalog, but we do not know if it was successfully compiled
          # until we test the validity.
          catalog = parallel_catalog_obj.output
          if catalog.valid?
            # The catalog was successfully compiled.
            result[key] = parallel_catalog_obj.output

            if task.args[:save_catalog]
              File.open(task.args[:save_catalog], 'w') { |f| f.write(catalog.catalog_json) }
              @logger.debug "Saved catalog to #{task.args[:save_catalog]}"
            end
          else
            # The catalog failed, but a catalog object was returned so that better error reporting
            # can take place. In this error reporting, we will replace 'Error:' with '[Puppet Error]'
            # and remove the compilation directory (which is a tmpdir) to reveal only the relative
            # path to the files involved.
            dir = catalog.compilation_dir || ''
            dir_regex = Regexp.new(Regexp.escape(dir) + '/environments/[^/]+/')
            error_display = catalog.error_message.split("\n").map do |line|
              line.sub(/^Error:/, '[Puppet Error]').gsub(dir_regex, '')
            end.join("\n")
            message = "Catalog for #{branch} failed to compile due to errors:\n#{error_display}"
            raise OctocatalogDiff::Errors::CatalogError, message
          end
        else
          # Something unhandled went wrong, and an exception was thrown. Reveal a generic message.
          # :nocov:
          msg = parallel_catalog_obj.exception.message
          message = "Catalog for '#{key}' (#{branch}) failed to compile with #{parallel_catalog_obj.exception.class}: #{msg}"
          message += "\n" + parallel_catalog_obj.exception.backtrace.map { |x| "   #{x}" }.join("\n") if @options[:debug]
          raise OctocatalogDiff::Errors::CatalogError, message
          # :nocov:
        end
      end

      # Performs the steps necessary to build a catalog.
      # @param opts [Hash] Options hash
      # @return [Hash] { :rc => exit code, :catalog => Catalog as JSON string }
      def build_catalog(opts, logger = @logger)
        logger.debug("Setting up Puppet catalog build for #{opts[:branch]}")
        catalog = opts[:object]
        logger.debug("Catalog for #{opts[:branch]} will be built with #{catalog.builder}")
        time_start = Time.now
        catalog.build(logger)
        time_it_took = Time.now - time_start
        retries_str = " retries = #{catalog.retries}" if catalog.retries.is_a?(Integer)
        time_str = "in #{time_it_took} seconds#{retries_str}"
        status_str = catalog.valid? ? 'successfully built' : 'failed'
        logger.debug "Catalog for #{opts[:branch]} #{status_str} with #{catalog.builder} #{time_str}"
        catalog
      end

      # The catalog validator method can indicate failure one of two ways:
      # - Raise an exception (this is preferred, since it gives a specific error message)
      # - Return false (supported but discouraged, since it only surfaces a generic error)
      # @param catalog [OctocatalogDiff::Catalog] Catalog object
      # @param logger [Logger] Logger object (presently unused)
      # @param args [Hash] Additional arguments set specifically for validator
      # @return [Boolean] Return true if catalog is valid, false otherwise
      def catalog_validator(catalog = nil, _logger = @logger, _args = {})
        raise ArgumentError, "Expects a catalog, got #{catalog.class}" unless catalog.is_a?(OctocatalogDiff::Catalog)
        raise OctocatalogDiff::Errors::CatalogError, "Catalog failed: #{catalog.error_message}" unless catalog.valid?
        true
      end
    end
  end
end