File: environment.rb

package info (click to toggle)
puppet-agent 8.10.0-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,404 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (638 lines) | stat: -rw-r--r-- 21,653 bytes parent folder | download | duplicates (2)
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# frozen_string_literal: true

require_relative '../../puppet/util'
require 'monitor'
require_relative '../../puppet/parser/parser_factory'
require_relative '../../puppet/concurrent/lock'

# Just define it, so this class has fewer load dependencies.
class Puppet::Node
end

# Puppet::Node::Environment acts as a container for all configuration
# that is expected to vary between environments.
#
# ## The root environment
#
# In addition to normal environments that are defined by the user,there is a
# special 'root' environment. It is defined as an instance variable on the
# Puppet::Node::Environment metaclass. The environment name is `*root*` and can
# be accessed by looking up the `:root_environment` using {Puppet.lookup}.
#
# The primary purpose of the root environment is to contain parser functions
# that are not bound to a specific environment. The main case for this is for
# logging functions. Logging functions are attached to the 'root' environment
# when {Puppet::Parser::Functions.reset} is called.
class Puppet::Node::Environment
  NO_MANIFEST = :no_manifest

  # The create() factory method should be used instead.
  private_class_method :new

  # Create a new environment with the given name
  #
  # @param name [Symbol] the name of the environment
  # @param modulepath [Array<String>] the list of paths from which to load modules
  # @param manifest [String] the path to the manifest for the environment or
  #   the constant Puppet::Node::Environment::NO_MANIFEST if there is none.
  # @param config_version [String] path to a script whose output will be added
  #   to report logs (optional)
  # @return [Puppet::Node::Environment]
  #
  # @api public
  def self.create(name, modulepath, manifest = NO_MANIFEST, config_version = nil)
    new(name, modulepath, manifest, config_version)
  end

  # A remote subclass to make it easier to trace instances when debugging.
  # @api private
  class Remote < Puppet::Node::Environment; end

  # A "reference" to a remote environment. The created environment instance
  # isn't expected to exist on the local system, but is instead a reference to
  # environment information on a remote system. For instance when a catalog is
  # being applied, this will be used on the agent.
  #
  # @note This does not provide access to the information of the remote
  # environment's modules, manifest, or anything else. It is simply a value
  # object to pass around and use as an environment.
  #
  # @param name [Symbol] The name of the remote environment
  #
  def self.remote(name)
    Remote.create(name, [], NO_MANIFEST)
  end

  # Instantiate a new environment
  #
  # @note {Puppet::Node::Environment.new} is private for historical reasons, as
  #   previously it had been overridden to return memoized objects and was
  #   replaced with {Puppet::Node::Environment.create}, so this will not be
  #   invoked with the normal Ruby initialization semantics.
  #
  # @param name [Symbol] The environment name
  def initialize(name, modulepath, manifest, config_version)
    @lock = Puppet::Concurrent::Lock.new
    @name = name.intern
    @modulepath = self.class.expand_dirs(self.class.extralibs() + modulepath)
    @manifest = manifest == NO_MANIFEST ? manifest : Puppet::FileSystem.expand_path(manifest)

    @config_version = config_version
  end

  # Creates a new Puppet::Node::Environment instance, overriding any of the passed
  # parameters.
  #
  # @param env_params [Hash<{Symbol => String,Array<String>}>] new environment
  #   parameters (:modulepath, :manifest, :config_version)
  # @return [Puppet::Node::Environment]
  def override_with(env_params)
    self.class.create(name,
                      env_params[:modulepath] || modulepath,
                      env_params[:manifest] || manifest,
                      env_params[:config_version] || config_version)
  end

  # Creates a new Puppet::Node::Environment instance, overriding :manifest,
  # :modulepath, or :config_version from the passed settings if they were
  # originally set from the commandline, or returns self if there is nothing to
  # override.
  #
  # @param settings [Puppet::Settings] an initialized puppet settings instance
  # @return [Puppet::Node::Environment] new overridden environment or self if
  #   there are no commandline changes from settings.
  def override_from_commandline(settings)
    overrides = {}

    if settings.set_by_cli?(:modulepath)
      overrides[:modulepath] = self.class.split_path(settings.value(:modulepath))
    end

    if settings.set_by_cli?(:config_version)
      overrides[:config_version] = settings.value(:config_version)
    end

    if settings.set_by_cli?(:manifest)
      overrides[:manifest] = settings.value(:manifest)
    end

    overrides.empty? ?
      self :
      override_with(overrides)
  end

  # @param [String] name Environment name to check for valid syntax.
  # @return [Boolean] true if name is valid
  # @api public
  def self.valid_name?(name)
    !!name.match(/\A\w+\Z/)
  end

  # @!attribute [r] name
  #   @api public
  #   @return [Symbol] the human readable environment name that serves as the
  #     environment identifier
  attr_reader :name

  # @api public
  # @return [Array<String>] All directories present on disk in the modulepath
  def modulepath
    @modulepath.find_all do |p|
      Puppet::FileSystem.directory?(p)
    end
  end

  # @api public
  # @return [Array<String>] All directories in the modulepath (even if they are not present on disk)
  def full_modulepath
    @modulepath
  end

  # @!attribute [r] manifest
  #   @api public
  #   @return [String] path to the manifest file or directory.
  attr_reader :manifest

  # @!attribute [r] config_version
  #   @api public
  #   @return [String] path to a script whose output will be added to report logs
  #     (optional)
  attr_reader :config_version

  # Cached loaders - management of value handled by Puppet::Pops::Loaders
  # @api private
  attr_accessor :loaders

  # Lock for compilation that needs exclusive access to the environment
  # @api private
  attr_reader :lock

  # For use with versioned dirs
  # our environment path may contain symlinks, while we want to resolve the
  # path while reading the manifests we may want to report the resources as
  # coming from the configured path.
  attr_accessor :configured_path

  # See :configured_path above
  attr_accessor :resolved_path

  # Ensure the path given is of the format we want in the catalog/report.
  #
  # Intended for use with versioned symlinked environments. If this
  # environment is configured with "/etc/puppetlabs/code/environments/production"
  # but the resolved path is
  #
  # "/opt/puppetlabs/server/puppetserver/filesync/client/puppet-code/production_abcdef1234"
  #
  # this changes the filepath
  #
  # "/opt/puppetlabs/server/puppetserver/filesync/client/puppet-code/production_abcdef1234/modules/foo/manifests/init.pp"
  #
  # to
  #
  # "/etc/puppetlabs/code/environments/production/modules/foo/manifests/init.pp"
  def externalize_path(filepath)
    paths_set        = configured_path && resolved_path
    munging_possible = paths_set && configured_path != resolved_path
    munging_desired  = munging_possible &&
                       Puppet[:report_configured_environmentpath] &&
                       filepath.to_s.start_with?(resolved_path)

    if munging_desired
      File.join(configured_path, filepath.delete_prefix(resolved_path))
    else
      filepath
    end
  end

  # Checks to make sure that this environment did not have a manifest set in
  # its original environment.conf if Puppet is configured with
  # +disable_per_environment_manifest+ set true.  If it did, the environment's
  # modules may not function as intended by the original authors, and we may
  # seek to halt a puppet compilation for a node in this environment.
  #
  # The only exception to this would be if the environment.conf manifest is an exact,
  # uninterpolated match for the current +default_manifest+ setting.
  #
  # @return [Boolean] true if using directory environments, and
  #   Puppet[:disable_per_environment_manifest] is true, and this environment's
  #   original environment.conf had a manifest setting that is not the
  #   Puppet[:default_manifest].
  # @api private
  def conflicting_manifest_settings?
    return false unless Puppet[:disable_per_environment_manifest]

    original_manifest = configuration.raw_setting(:manifest)
    !original_manifest.nil? && !original_manifest.empty? && original_manifest != Puppet[:default_manifest]
  end

  # @api private
  def static_catalogs?
    if @static_catalogs.nil?
      environment_conf = Puppet.lookup(:environments).get_conf(name)
      @static_catalogs = (environment_conf.nil? ? Puppet[:static_catalogs] : environment_conf.static_catalogs)
    end
    @static_catalogs
  end

  # Return the environment configuration
  # @return [Puppet::Settings::EnvironmentConf] The configuration
  #
  # @api private
  def configuration
    Puppet.lookup(:environments).get_conf(name)
  end

  # Checks the environment and settings for any conflicts
  # @return [Array<String>] an array of validation errors
  # @api public
  def validation_errors
    errors = []
    if conflicting_manifest_settings?
      errors << _("The 'disable_per_environment_manifest' setting is true, and the '%{env_name}' environment has an environment.conf manifest that conflicts with the 'default_manifest' setting.") % { env_name: name }
    end
    errors
  end

  def rich_data_from_env_conf
    unless @checked_conf_for_rich_data
      environment_conf = Puppet.lookup(:environments).get_conf(name)
      @rich_data_from_conf = environment_conf&.rich_data
      @checked_conf_for_rich_data = true
    end
    @rich_data_from_conf
  end

  # Checks if this environment permits use of rich data types in the catalog
  # Checks the environment conf for an override on first query, then going forward
  # either uses that, or if unset, uses the current value of the `rich_data` setting.
  # @return [Boolean] `true` if rich data is permitted.
  # @api private
  def rich_data?
    @rich_data = rich_data_from_env_conf.nil? ? Puppet[:rich_data] : rich_data_from_env_conf
  end

  # Return an environment-specific Puppet setting.
  #
  # @api public
  #
  # @param param [String, Symbol] The environment setting to look up
  # @return [Object] The resolved setting value
  def [](param)
    Puppet.settings.value(param, name)
  end

  # @api public
  # @return [Puppet::Resource::TypeCollection] The current global TypeCollection
  def known_resource_types
    @lock.synchronize do
      if @known_resource_types.nil?
        @known_resource_types = Puppet::Resource::TypeCollection.new(self)
        @known_resource_types.import_ast(perform_initial_import(), '')
      end
      @known_resource_types
    end
  end

  # Yields each modules' plugin directory if the plugin directory (modulename/lib)
  # is present on the filesystem.
  #
  # @yield [String] Yields the plugin directory from each module to the block.
  # @api public
  def each_plugin_directory(&block)
    modules.map(&:plugin_directory).each do |lib|
      lib = Puppet::Util::Autoload.cleanpath(lib)
      yield lib if File.directory?(lib)
    end
  end

  # Locate a module instance by the module name alone.
  #
  # @api public
  #
  # @param name [String] The module name
  # @return [Puppet::Module, nil] The module if found, else nil
  def module(name)
    modules_by_name[name]
  end

  # Locate a module instance by the full forge name (EG authorname/module)
  #
  # @api public
  #
  # @param forge_name [String] The module name
  # @return [Puppet::Module, nil] The module if found, else nil
  def module_by_forge_name(forge_name)
    _, modname = forge_name.split('/')
    found_mod = self.module(modname)
    found_mod and found_mod.forge_name == forge_name ?
      found_mod :
      nil
  end

  # Return all modules for this environment in the order they appear in the
  # modulepath.
  # @note If multiple modules with the same name are present they will
  #   both be added, but methods like {#module} and {#module_by_forge_name}
  #   will return the first matching entry in this list.
  # @note This value is cached so that the filesystem doesn't have to be
  #   re-enumerated every time this method is invoked, since that
  #   enumeration could be a costly operation and this method is called
  #   frequently. The cache expiry is determined by `Puppet[:filetimeout]`.
  # @api public
  # @return [Array<Puppet::Module>] All modules for this environment
  def modules
    if @modules.nil?
      module_references = []
      project = Puppet.lookup(:bolt_project) { nil }
      seen_modules = if project && project.load_as_module?
                       module_references << project.to_h
                       { project.name => true }
                     else
                       {}
                     end
      modulepath.each do |path|
        Puppet::FileSystem.children(path).map do |p|
          Puppet::FileSystem.basename_string(p)
        end.each do |name|
          next unless Puppet::Module.is_module_directory?(name, path)

          warn_about_mistaken_path(path, name)
          unless seen_modules[name]
            module_references << { :name => name, :path => File.join(path, name) }
            seen_modules[name] = true
          end
        end
      end

      @modules = module_references.filter_map do |reference|
        Puppet::Module.new(reference[:name], reference[:path], self)
      rescue Puppet::Module::Error => e
        Puppet.log_exception(e)
        nil
      end
    end
    @modules
  end

  # @api private
  def modules_by_name
    @modules_by_name ||= modules.to_h { |mod| [mod.name, mod] }
  end
  private :modules_by_name

  # Generate a warning if the given directory in a module path entry is named `lib`.
  #
  # @api private
  #
  # @param path [String] The module directory containing the given directory
  # @param name [String] The directory name
  def warn_about_mistaken_path(path, name)
    if name == "lib"
      Puppet.debug {
        "Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless you \
        are expecting to load a module named 'lib', your module path may be set incorrectly."
      }
    end
  end

  # Modules broken out by directory in the modulepath
  #
  # @api public
  #
  # @return [Hash<String, Array<Puppet::Module>>] A hash whose keys are file
  #   paths, and whose values is an array of Puppet Modules for that path
  def modules_by_path
    modules_by_path = {}
    modulepath.each do |path|
      if Puppet::FileSystem.exist?(path)
        module_names = Puppet::FileSystem.children(path).map do |p|
          Puppet::FileSystem.basename_string(p)
        end.select do |name|
          Puppet::Module.is_module_directory?(name, path)
        end
        modules_by_path[path] = module_names.sort.map do |name|
          Puppet::Module.new(name, File.join(path, name), self)
        end
      else
        modules_by_path[path] = []
      end
    end
    modules_by_path
  end

  # All module requirements for all modules in the environment modulepath
  #
  # @api public
  #
  # @comment This has nothing to do with an environment. It seems like it was
  #   stuffed into the first convenient class that vaguely involved modules.
  #
  # @example
  #   environment.module_requirements
  #   # => {
  #   #   'username/amodule' => [
  #   #     {
  #   #       'name'    => 'username/moduledep',
  #   #       'version' => '1.2.3',
  #   #       'version_requirement' => '>= 1.0.0',
  #   #     },
  #   #     {
  #   #       'name'    => 'username/anotherdep',
  #   #       'version' => '4.5.6',
  #   #       'version_requirement' => '>= 3.0.0',
  #   #     }
  #   #   ]
  #   # }
  #   #
  #
  # @return [Hash<String, Array<Hash<String, String>>>] See the method example
  #   for an explanation of the return value.
  def module_requirements
    deps = {}

    modules.each do |mod|
      next unless mod.forge_name

      deps[mod.forge_name] ||= []

      mod.dependencies and mod.dependencies.each do |mod_dep|
        dep_name = mod_dep['name'].tr('-', '/')
        (deps[dep_name] ||= []) << {
          'name' => mod.forge_name,
          'version' => mod.version,
          'version_requirement' => mod_dep['version_requirement']
        }
      end
    end

    deps.each do |mod, mod_deps|
      deps[mod] = mod_deps.sort_by { |d| d['name'] }
    end

    deps
  end

  # Loads module translations for the current environment once for
  # the lifetime of the environment. Execute a block in the context
  # of that translation domain.
  def with_text_domain
    return yield if Puppet[:disable_i18n]

    if @text_domain.nil?
      @text_domain = @name
      Puppet::GettextConfig.reset_text_domain(@text_domain)
      Puppet::ModuleTranslations.load_from_modulepath(modules)
    else
      Puppet::GettextConfig.use_text_domain(@text_domain)
    end

    yield
  ensure
    # Is a noop if disable_i18n is true
    Puppet::GettextConfig.clear_text_domain
  end

  # Checks if a reparse is required (cache of files is stale).
  #
  def check_for_reparse
    @lock.synchronize do
      if Puppet[:code] != @parsed_code || @known_resource_types.parse_failed?
        @parsed_code = nil
        @known_resource_types = nil
      end
    end
  end

  # @return [String] The YAML interpretation of the object
  # Return the name of the environment as a string interpretation of the object
  def to_yaml
    to_s.to_yaml
  end

  # @return [String] The stringified value of the `name` instance variable
  # @api public
  def to_s
    name.to_s
  end

  # @api public
  def inspect
    %Q(<#{self.class}:#{object_id} @name="#{name}" @manifest="#{manifest}" @modulepath="#{full_modulepath.join(':')}" >)
  end

  # @return [Symbol] The `name` value, cast to a string, then cast to a symbol.
  #
  # @api public
  #
  # @note the `name` instance variable is a Symbol, but this casts the value
  #   to a String and then converts it back into a Symbol which will needlessly
  #   create an object that needs to be garbage collected
  def to_sym
    to_s.to_sym
  end

  def self.split_path(path_string)
    path_string.split(File::PATH_SEPARATOR)
  end

  def ==(other)
    true if other.is_a?(Puppet::Node::Environment) &&
            name == other.name &&
            full_modulepath == other.full_modulepath &&
            manifest == other.manifest
  end

  alias eql? ==

  def hash
    [self.class, name, full_modulepath, manifest].hash
  end

  # not private so it can be called in tests
  def self.extralibs
    if ENV['PUPPETLIB']
      split_path(ENV.fetch('PUPPETLIB', nil))
    else
      []
    end
  end

  # not private so it can be called in initialize
  def self.expand_dirs(dirs)
    dirs.collect do |dir|
      Puppet::FileSystem.expand_path(dir)
    end
  end

  private

  # Reparse the manifests for the given environment
  #
  # There are two sources that can be used for the initial parse:
  #
  #   1. The value of `Puppet[:code]`: Puppet can take a string from
  #     its settings and parse that as a manifest. This is used by various
  #     Puppet applications to read in a manifest and pass it to the
  #     environment as a side effect. This is attempted first.
  #   2. The contents of this environment's +manifest+ attribute: Puppet will
  #     try to load the environment manifest.
  #
  # @return [Puppet::Parser::AST::Hostclass] The AST hostclass object
  #   representing the 'main' hostclass
  def perform_initial_import
    parser = Puppet::Parser::ParserFactory.parser
    @parsed_code = Puppet[:code]
    if @parsed_code != ""
      parser.string = @parsed_code
      parser.parse
    else
      file = manifest
      # if the manifest file is a reference to a directory, parse and combine
      # all .pp files in that directory
      if file == NO_MANIFEST
        empty_parse_result
      elsif File.directory?(file)
        # JRuby does not properly perform Dir.glob operations with wildcards, (see PUP-11788 and https://github.com/jruby/jruby/issues/7836).
        # We sort the results because Dir.glob order is inconsistent in Ruby < 3 (see PUP-10115).
        parse_results = Puppet::FileSystem::PathPattern.absolute(File.join(file, '**/*')).glob.select { |globbed_file| globbed_file.end_with?('.pp') }.sort.map do |file_to_parse|
          parser.file = file_to_parse
          parser.parse
        end
        # Use a parser type specific merger to concatenate the results
        Puppet::Parser::AST::Hostclass.new('', :code => Puppet::Parser::ParserFactory.code_merger.concatenate(parse_results))
      else
        parser.file = file
        parser.parse
      end
    end
  rescue Puppet::ParseErrorWithIssue => detail
    @known_resource_types.parse_failed = true
    detail.environment = name
    raise
  rescue => detail
    @known_resource_types.parse_failed = true

    msg = _("Could not parse for environment %{env}: %{detail}") % { env: self, detail: detail }
    error = Puppet::Error.new(msg)
    error.set_backtrace(detail.backtrace)
    raise error
  end

  # Return an empty top-level hostclass to indicate that no file was loaded
  #
  # @return [Puppet::Parser::AST::Hostclass]
  def empty_parse_result
    Puppet::Parser::AST::Hostclass.new('')
  end

  # A None subclass to make it easier to trace the NONE environment when debugging.
  # @api private
  class None < Puppet::Node::Environment; end

  # A special "null" environment
  #
  # This environment should be used when there is no specific environment in
  # effect.
  NONE = None.create(:none, [])
end