File: manager.rb

package info (click to toggle)
vagrant 2.2.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 8,072 kB
  • sloc: ruby: 80,731; sh: 369; makefile: 9; lisp: 1
file content (374 lines) | stat: -rw-r--r-- 12,539 bytes parent folder | download
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
require "pathname"
require "set"

require_relative "../bundler"
require_relative "../shared_helpers"
require_relative "state_file"

module Vagrant
  module Plugin
    # The Manager helps with installing, listing, and initializing plugins.
    class Manager
      # Returns the path to the [StateFile] for user plugins.
      #
      # @return [Pathname]
      def self.user_plugins_file
        Vagrant.user_data_path.join("plugins.json")
      end

      # Returns the path to the [StateFile] for system plugins.
      def self.system_plugins_file
        dir = '/usr/share/vagrant-plugins'
        return nil if !dir
        Pathname.new(dir).join("plugins.json")
      end

      def self.instance
        @instance ||= self.new(user_plugins_file)
      end

      attr_reader :user_file
      attr_reader :system_file
      attr_reader :local_file

      # @param [Pathname] user_file
      def initialize(user_file)
        @logger = Log4r::Logger.new("vagrant::plugin::manager")
        @user_file   = StateFile.new(user_file)

        system_path  = self.class.system_plugins_file
        @system_file = nil
        @system_file = StateFile.new(system_path, true) if system_path && system_path.file?

        @local_file = nil
        @globalized = @localized = false
      end

      # Enable global plugins
      #
      # @return [Hash] list of plugins
      def globalize!
        @globalized = true
        @logger.debug("Enabling globalized plugins")
        plugins = installed_plugins
        bundler_init(plugins)
        plugins
      end

      # Enable environment local plugins
      #
      # @param [Environment] env Vagrant environment
      # @return [Hash, nil] list of plugins
      def localize!(env)
        @localized = true
        if env.local_data_path
          @logger.debug("Enabling localized plugins")
          @local_file = StateFile.new(env.local_data_path.join("plugins.json"))
          Vagrant::Bundler.instance.environment_path = env.local_data_path
          plugins = local_file.installed_plugins
          bundler_init(plugins)
          plugins
        end
      end

      # @return [Boolean] local and global plugins are loaded
      def ready?
        @globalized && @localized
      end

      # Initialize bundler with given plugins
      #
      # @param [Hash] plugins List of plugins
      # @return [nil]
      def bundler_init(plugins)
        if !Vagrant.plugins_init?
          @logger.warn("Plugin initialization is disabled")
          return nil
        end

        @logger.info("Plugins:")
        plugins.each do |plugin_name, plugin_info|
          installed_version = plugin_info["installed_gem_version"]
          version_constraint = plugin_info["gem_version"]
          installed_version = 'undefined' if installed_version.to_s.empty?
          version_constraint = '> 0' if version_constraint.to_s.empty?
          @logger.info(
            "  - #{plugin_name} = [installed: " \
              "#{installed_version} constraint: " \
              "#{version_constraint}]"
          )
        end
        begin
          Vagrant::Bundler.instance.init!(plugins)
        rescue StandardError, ScriptError => err
          @logger.error("Plugin initialization error - #{err.class}: #{err}")
          err.backtrace.each do |backtrace_line|
            @logger.debug(backtrace_line)
          end
          raise Vagrant::Errors::PluginInitError, message: err.to_s
        end
      end

      # Installs another plugin into our gem directory.
      #
      # @param [String] name Name of the plugin (gem)
      # @return [Gem::Specification]
      def install_plugin(name, **opts)
        if opts[:env_local] && @local_file.nil?
          raise Errors::PluginNoLocalError
        end

        if name =~ /\.gem$/
          # If this is a gem file, then we install that gem locally.
          local_spec = Vagrant::Bundler.instance.install_local(name, opts)
          name       = local_spec.name
          opts[:version] = local_spec.version.to_s
        end

        plugins = installed_plugins
        plugins[name] = {
          "require"     => opts[:require],
          "gem_version" => opts[:version],
          "sources"     => opts[:sources],
        }

        if local_spec.nil?
          result = nil
          install_lambda = lambda do
            Vagrant::Bundler.instance.install(plugins, opts[:env_local]).each do |spec|
              next if spec.name != name
              next if result && result.version >= spec.version
              result = spec
            end
          end

          if opts[:verbose]
            Vagrant::Bundler.instance.verbose(&install_lambda)
          else
            install_lambda.call
          end
        else
          result = local_spec
        end
        # Add the plugin to the state file
        plugin_file = opts[:env_local] ? @local_file : @user_file
        plugin_file.add_plugin(
          result.name,
          version: opts[:version],
          require: opts[:require],
          sources: opts[:sources],
          env_local: !!opts[:env_local],
          installed_gem_version: result.version.to_s
        )

        # After install clean plugin gems to remove any cruft. This is useful
        # for removing outdated dependencies or other versions of an installed
        # plugin if the plugin is upgraded/downgraded
        Vagrant::Bundler.instance.clean(installed_plugins, local: !!opts[:local])
        result
      rescue Gem::GemNotFoundException
        raise Errors::PluginGemNotFound, name: name
      rescue Gem::Exception => e
        raise Errors::BundlerError, message: e.to_s
      end

      # Uninstalls the plugin with the given name.
      #
      # @param [String] name
      def uninstall_plugin(name, **opts)
        if @system_file
          if !@user_file.has_plugin?(name) && @system_file.has_plugin?(name)
            raise Errors::PluginUninstallSystem,
              name: name
          end
        end

        if opts[:env_local] && @local_file.nil?
          raise Errors::PluginNoLocalError
        end

        plugin_file = opts[:env_local] ? @local_file : @user_file

        if !plugin_file.has_plugin?(name)
          raise Errors::PluginNotInstalled,
            name: name
        end

        plugin_file.remove_plugin(name)

        # Clean the environment, removing any old plugins
        Vagrant::Bundler.instance.clean(installed_plugins)
      rescue Gem::Exception => e
        raise Errors::BundlerError, message: e.to_s
      end

      # Updates all or a specific set of plugins.
      def update_plugins(specific, **opts)
        if opts[:env_local] && @local_file.nil?
          raise Errors::PluginNoLocalError
        end

        plugin_file = opts[:env_local] ? @local_file : @user_file

        result = Vagrant::Bundler.instance.update(plugin_file.installed_plugins, specific)
        plugin_file.installed_plugins.each do |name, info|
          matching_spec = result.detect{|s| s.name == name}
          info = Hash[
            info.map do |key, value|
              [key.to_sym, value]
            end
          ]
          if matching_spec
            plugin_file.add_plugin(name, **info.merge(
              version: "> 0",
              installed_gem_version: matching_spec.version.to_s
            ))
          end
        end
        Vagrant::Bundler.instance.clean(installed_plugins)
        result
      rescue Gem::Exception => e
        raise Errors::BundlerError, message: e.to_s
      end

      # This returns the list of plugins that should be enabled.
      #
      # @return [Hash]
      def installed_plugins
        system = {}
        if @system_file
          @system_file.installed_plugins.each do |k, v|
            system[k] = v.merge("system" => true)
          end
        end
        plugin_list = Util::DeepMerge.deep_merge(system, @user_file.installed_plugins)

        if @local_file
          plugin_list = Util::DeepMerge.deep_merge(plugin_list,
            @local_file.installed_plugins)
        end

        # Sort plugins by name
        Hash[
          plugin_list.map{|plugin_name, plugin_info|
            [plugin_name, plugin_info]
          }.sort_by(&:first)
        ]
      end

      # This returns the list of plugins that are installed as
      # Gem::Specifications.
      #
      # @return [Array<Gem::Specification>]
      def installed_specs
        installed_plugin_info = installed_plugins
        installed = Set.new(installed_plugin_info.keys)
        installed_versions = Hash[
          installed_plugin_info.map{|plugin_name, plugin_info|
            gem_version = plugin_info["gem_version"].to_s
            gem_version = "> 0" if gem_version.empty?
            [plugin_name, Gem::Requirement.new(gem_version)]
          }
        ]

        # Go through the plugins installed in this environment and
        # get the latest version of each.
        installed_map = {}
        Gem::Specification.find_all.each do |spec|
          # Ignore specs that aren't in our installed list
          next if !installed.include?(spec.name)

          next if installed_versions[spec.name] &&
            !installed_versions[spec.name].satisfied_by?(spec.version)

          # If we already have a newer version in our list of installed,
          # then ignore it
          next if installed_map.key?(spec.name) &&
            installed_map[spec.name].version >= spec.version

          installed_map[spec.name] = spec
        end

        installed_map.values
      end

      # Loads the requested plugins into the Vagrant runtime
      #
      # @param [Hash] plugins List of plugins to load
      # @return [nil]
      def load_plugins(plugins)
        if !Vagrant.plugins_enabled?
          @logger.warn("Plugin loading is disabled")
          return
        end

        if plugins.nil?
          @logger.debug("No plugins provided for loading")
          return
        end

        begin
          @logger.info("Loading plugins...")
          plugins.each do |plugin_name, plugin_info|
            if plugin_info["require"].to_s.empty?
              begin
                @logger.info("Loading plugin `#{plugin_name}` with default require: `#{plugin_name}`")
                require plugin_name
              rescue LoadError => err
                if plugin_name.include?("-")
                  plugin_slash = plugin_name.gsub("-", "/")
                  @logger.error("Failed to load plugin `#{plugin_name}` with default require. - #{err.class}: #{err}")
                  @logger.info("Loading plugin `#{plugin_name}` with slash require: `#{plugin_slash}`")
                  require plugin_slash
                else
                  raise
                end
              end
            else
              @logger.debug("Loading plugin `#{plugin_name}` with custom require: `#{plugin_info["require"]}`")
              require plugin_info["require"]
            end
            @logger.debug("Successfully loaded plugin `#{plugin_name}`.")
          end
          if defined?(::Bundler)
            @logger.debug("Bundler detected in use. Loading `:plugins` group.")
            ::Bundler.require(:plugins)
          end
        rescue ScriptError, StandardError => err
          @logger.error("Plugin loading error: #{err.class} - #{err}")
          err.backtrace.each do |backtrace_line|
            @logger.debug(backtrace_line)
          end
          raise Vagrant::Errors::PluginLoadError, message: err.to_s
        end
        nil
      end

      # Check if the requested plugin is installed
      #
      # @param [String] name Name of plugin
      # @param [String] version Specific version of the plugin
      # @return [Boolean]
      def plugin_installed?(name, version=nil)
        # Make the requirement object
        version = Gem::Requirement.new([version.to_s]) if version

        # If plugins are loaded, check for match in loaded specs
        if ready?
          return installed_specs.any? do |s|
            match = s.name == name
            next match if !version
            next match && version.satisfied_by?(s.version)
          end
        end

        # Plugins are not loaded yet so check installed plugin data
        plugin_info = installed_plugins[name]
        return false if !plugin_info
        return !!plugin_info if version.nil? || plugin_info["installed_gem_version"].nil?
        installed_version = Gem::Version.new(plugin_info["installed_gem_version"])
        version.satisfied_by?(installed_version)
      end
    end
  end
end