File: plan.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 (166 lines) | stat: -rw-r--r-- 5,151 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
# frozen_string_literal: true

require_relative '../../puppet/util/logging'

class Puppet::Module
  class Plan
    class Error < Puppet::Error
      attr_accessor :kind, :details

      def initialize(message, kind, details = nil)
        super(message)
        @details = details || {}
        @kind = kind
      end

      def to_h
        {
          msg: message,
          kind: kind,
          details: details
        }
      end
    end

    class InvalidName < Error
      def initialize(name, msg)
        super(msg, 'puppet.plans/invalid-name')
      end
    end

    class InvalidFile < Error
      def initialize(msg)
        super(msg, 'puppet.plans/invalid-file')
      end
    end

    class InvalidPlan < Error
    end

    class InvalidMetadata < Error
    end

    class PlanNotFound < Error
      def initialize(plan_name, module_name)
        msg = _("Plan %{plan_name} not found in module %{module_name}.") %
              { plan_name: plan_name, module_name: module_name }
        super(msg, 'puppet.plans/plan-not-found', { 'name' => plan_name })
      end
    end

    ALLOWED_EXTENSIONS = %w[.pp .yaml]
    RESERVED_WORDS = %w[and application attr case class consumes default else
                        elsif environment false function if import in inherits node or private
                        produces site true type undef unless]
    RESERVED_DATA_TYPES = %w[any array boolean catalogentry class collection
                             callable data default enum float hash integer numeric optional pattern
                             resource runtime scalar string struct tuple type undef variant]

    def self.is_plan_name?(name)
      return true if name =~ /^[a-z][a-z0-9_]*$/

      false
    end

    # Determine whether a plan file has a legal name and extension
    def self.is_plans_filename?(path)
      name = File.basename(path, '.*')
      ext = File.extname(path)
      return [false, _("Plan names must start with a lowercase letter and be composed of only lowercase letters, numbers, and underscores")] unless is_plan_name?(name)
      unless ALLOWED_EXTENSIONS.include? ext
        return [false, _("Plan name cannot have extension %{ext}, must be .pp or .yaml") % { ext: ext }]
      end
      if RESERVED_WORDS.include?(name)
        return [false, _("Plan name cannot be a reserved word, but was '%{name}'") % { name: name }]
      end
      if RESERVED_DATA_TYPES.include?(name)
        return [false, _("Plan name cannot be a Puppet data type, but was '%{name}'") % { name: name }]
      end

      [true]
    end

    # Executables list should contain the full path of all possible implementation files
    def self.find_implementations(name, plan_files)
      basename = name.split('::')[1] || 'init'

      # If implementations isn't defined, then we use executables matching the
      # plan name, and only one may exist.
      implementations = plan_files.select { |impl| File.basename(impl, '.*') == basename }

      # Select .pp before .yaml, since .pp comes before .yaml alphabetically.
      chosen = implementations.min

      [{ "name" => File.basename(chosen), "path" => chosen }]
    end
    private_class_method :find_implementations

    def self.find_files(name, plan_files)
      find_implementations(name, plan_files)
    end

    def self.plans_in_module(pup_module)
      # Search e.g. 'modules/<pup_module>/plans' for all plans
      plan_files = Dir.glob(File.join(pup_module.plans_directory, '*'))
                      .keep_if { |f| valid, _ = is_plans_filename?(f); valid }

      plans = plan_files.group_by { |f| plan_name_from_path(f) }

      plans.map do |plan, plan_filenames|
        new_with_files(pup_module, plan, plan_filenames)
      end
    end

    attr_reader :name, :module, :metadata_file

    # file paths must be relative to the modules plan directory
    def initialize(pup_module, plan_name, plan_files)
      valid, reason = Puppet::Module::Plan.is_plans_filename?(plan_files.first)
      unless valid
        raise InvalidName.new(plan_name, reason)
      end

      name = plan_name == "init" ? pup_module.name : "#{pup_module.name}::#{plan_name}"

      @module = pup_module
      @name = name
      @metadata_file = metadata_file
      @plan_files = plan_files || []
    end

    def metadata
      # Nothing to go here unless plans eventually support metadata.
      @metadata ||= {}
    end

    def files
      @files ||= self.class.find_files(@name, @plan_files)
    end

    def validate
      files
      true
    end

    def ==(other)
      name == other.name &&
        self.module == other.module
    end

    def environment_name
      @module.environment.respond_to?(:name) ? @module.environment.name : 'production'
    end
    private :environment_name

    def self.new_with_files(pup_module, name, plan_files)
      Puppet::Module::Plan.new(pup_module, name, plan_files)
    end
    private_class_method :new_with_files

    # Abstracted here so we can add support for subdirectories later
    def self.plan_name_from_path(path)
      File.basename(path, '.*')
    end
    private_class_method :plan_name_from_path
  end
end