From ff86d86ac861c94d8eccb6bdb317d939ad6d4f0e Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Wed, 9 Feb 2022 19:15:01 -0600 Subject: [PATCH] Provisioner Plugins --- builtin/myplugin/proto/plugin.pb.go | 2 +- .../proto/ruby_vagrant/ruby-server.pb.go | 5 +- .../proto/ruby_vagrant/ruby-server.proto | 1 + .../server/proto/vagrant_server/server.pb.go | 5 +- lib/vagrant/plugin/remote.rb | 1 + lib/vagrant/plugin/remote/manager.rb | 28 ++-- lib/vagrant/plugin/remote/provisioner.rb | 47 +++++++ lib/vagrant/plugin/v2/config.rb | 14 ++ .../proto/ruby_vagrant/ruby-server_pb.rb | 1 + plugins/commands/serve/client.rb | 1 + plugins/commands/serve/client/provisioner.rb | 47 +++++++ plugins/commands/serve/command.rb | 2 +- plugins/commands/serve/mappers.rb | 1 + plugins/commands/serve/mappers/provisioner.rb | 49 +++++++ plugins/commands/serve/service.rb | 1 + .../serve/service/provisioner_service.rb | 130 ++++++++++++++++++ plugins/commands/serve/util/service_info.rb | 2 +- test/unit/vagrant/plugin/v2/config_test.rb | 7 + 18 files changed, 324 insertions(+), 20 deletions(-) create mode 100644 lib/vagrant/plugin/remote/provisioner.rb create mode 100644 plugins/commands/serve/client/provisioner.rb create mode 100644 plugins/commands/serve/mappers/provisioner.rb create mode 100644 plugins/commands/serve/service/provisioner_service.rb diff --git a/builtin/myplugin/proto/plugin.pb.go b/builtin/myplugin/proto/plugin.pb.go index 6dd03bd33..6518fd246 100644 --- a/builtin/myplugin/proto/plugin.pb.go +++ b/builtin/myplugin/proto/plugin.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.18.0 +// protoc v3.15.8 // source: vagrant-ruby/builtin/myplugin/proto/plugin.proto package proto diff --git a/internal/server/proto/ruby_vagrant/ruby-server.pb.go b/internal/server/proto/ruby_vagrant/ruby-server.pb.go index 78677ac4a..af772ba3b 100644 --- a/internal/server/proto/ruby_vagrant/ruby-server.pb.go +++ b/internal/server/proto/ruby_vagrant/ruby-server.pb.go @@ -1,13 +1,14 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.19.4 +// protoc v3.15.8 // source: proto/ruby_vagrant/ruby-server.proto package ruby_vagrant import ( vagrant_plugin_sdk "github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk" + _ "google.golang.org/genproto/googleapis/rpc/errdetails" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" _ "google.golang.org/protobuf/types/known/anypb" @@ -321,6 +322,8 @@ var file_proto_ruby_vagrant_ruby_server_proto_rawDesc = []byte{ 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0c, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x49, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, diff --git a/internal/server/proto/ruby_vagrant/ruby-server.proto b/internal/server/proto/ruby_vagrant/ruby-server.proto index 6f09eff5b..ff716f985 100644 --- a/internal/server/proto/ruby_vagrant/ruby-server.proto +++ b/internal/server/proto/ruby_vagrant/ruby-server.proto @@ -6,6 +6,7 @@ option go_package = "github.com/hashicorp/vagrant/internal/server/proto/ruby_vag import "google/protobuf/empty.proto"; import "google/protobuf/any.proto"; +import "google/rpc/error_details.proto"; import "plugin.proto"; diff --git a/internal/server/proto/vagrant_server/server.pb.go b/internal/server/proto/vagrant_server/server.pb.go index 23fd6bab0..dd7b455b3 100644 --- a/internal/server/proto/vagrant_server/server.pb.go +++ b/internal/server/proto/vagrant_server/server.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.19.4 +// protoc v3.15.8 // source: proto/vagrant_server/server.proto package vagrant_server @@ -9610,7 +9610,8 @@ func (*GetJobStreamResponse_Terminal_Event_Raw_) isGetJobStreamResponse_Terminal func (*GetJobStreamResponse_Terminal_Event_Table_) isGetJobStreamResponse_Terminal_Event_Event() {} -func (*GetJobStreamResponse_Terminal_Event_StepGroup_) isGetJobStreamResponse_Terminal_Event_Event() {} +func (*GetJobStreamResponse_Terminal_Event_StepGroup_) isGetJobStreamResponse_Terminal_Event_Event() { +} func (*GetJobStreamResponse_Terminal_Event_Step_) isGetJobStreamResponse_Terminal_Event_Event() {} diff --git a/lib/vagrant/plugin/remote.rb b/lib/vagrant/plugin/remote.rb index c97844acc..a4cce63a8 100644 --- a/lib/vagrant/plugin/remote.rb +++ b/lib/vagrant/plugin/remote.rb @@ -9,6 +9,7 @@ module Vagrant autoload :Plugin, "vagrant/plugin/remote/plugin" autoload :Provider, "vagrant/plugin/remote/provider" autoload :Push, "vagrant/plugin/remote/push" + autoload :Provisioner, "vagrant/plugin/remote/provisioner" autoload :SyncedFolder, "vagrant/plugin/remote/synced_folder" end end diff --git a/lib/vagrant/plugin/remote/manager.rb b/lib/vagrant/plugin/remote/manager.rb index 11d6551b6..918f0e528 100644 --- a/lib/vagrant/plugin/remote/manager.rb +++ b/lib/vagrant/plugin/remote/manager.rb @@ -190,20 +190,20 @@ module Vagrant end end - # def provisioners - # return real_manager.provisioners if plugin_manager.nil? - # - # Registry.new.tap do |result| - # plugin_manager.list_plugins(:provisioner).each do |plg| - # sf_class = Class.new(Remote::Provisioner, &WRAPPER_CLASS) - # sf_class.plugin_name = plg[:name] - # sf_class.type = plg[:type] - # result.register(plg[:name].to_sym) do - # sf_class - # end - # end - # end - # end + def provisioners + return real_manager.provisioners if plugin_manager.nil? + + Registry.new.tap do |result| + plugin_manager.list_plugins(:provisioner).each do |plg| + sf_class = Class.new(Remote::Provisioner, &WRAPPER_CLASS) + sf_class.plugin_name = plg[:name] + sf_class.type = plg[:type] + result.register(plg[:name].to_sym) do + sf_class + end + end + end + end def pushes return real_manager.pushes if plugin_manager.nil? diff --git a/lib/vagrant/plugin/remote/provisioner.rb b/lib/vagrant/plugin/remote/provisioner.rb new file mode 100644 index 000000000..7f4f40209 --- /dev/null +++ b/lib/vagrant/plugin/remote/provisioner.rb @@ -0,0 +1,47 @@ +module Vagrant + module Plugin + module Remote + class Provisioner < V2::Provisioner + # Add an attribute accesor for the client + attr_accessor :client + + # Provisioner plugins receive machine and config on init that they + # expect to set as instance variables and use later. When we're in + # server mode, this instance is only a client, and the local plugin + # is going to be reinstantiated over on the server side. We will still + # needs these two pieces of state on the server side, so in order to + # get them over there, we tack them onto the front as args for each + # client method call. The server will then peel off those two args + # and use them to initialize the plugin in local mode before it + # dispatches the method call. + # + # @see Vagrant::Plugin::V2::Provisioner#initialize the local init + # method we're overriding + # @see Vagrant::CommandServe::Service::ProvisionerService where we + # pop off the two args and reinitialize a local plugin + def initialize(machine, config, **opts) + @_machine = machine + @_config = config + if opts[:client].nil? + raise ArgumentError, + "Remote client is required for `#{self.class.name}`" + end + @client = opts[:client] + super(machine, config) + end + + def cleanup + client.provision(@_machine, @_config) + end + + def configure(root_config) + client.configure(@_machine, @_config, root_config) + end + + def provision + client.provision(@_machine, @_config) + end + end + end + end +end diff --git a/lib/vagrant/plugin/v2/config.rb b/lib/vagrant/plugin/v2/config.rb index 08e3ec88e..3447ab4cc 100644 --- a/lib/vagrant/plugin/v2/config.rb +++ b/lib/vagrant/plugin/v2/config.rb @@ -76,6 +76,20 @@ module Vagrant def method_missing(name, *args, &block) return super if @__finalized + # There are a few scenarios where ruby will attempt to implicity + # coerce a given object into a certain type. Configs can end up + # in some of these scenarios when they're being shipped around in + # callbacks with splats. If method_missing allows these methods to be + # called but continues to return Config back, Ruby will raise a + # TypeError. Doing the normal thing of raising NoMethodError allows + # Config to behave normally as its being passed through splats. + # + # For a bit more detail and some keywords for further searching, see: + # https://ruby-doc.org/core-2.7.2/doc/implicit_conversion_rdoc.html + if [:to_hash, :to_ary].include?(name) + return super + end + name = name.to_s name = name[0...-1] if name.end_with?("=") diff --git a/lib/vagrant/protobufs/proto/ruby_vagrant/ruby-server_pb.rb b/lib/vagrant/protobufs/proto/ruby_vagrant/ruby-server_pb.rb index 3df4f8b5f..e36c32cbe 100644 --- a/lib/vagrant/protobufs/proto/ruby_vagrant/ruby-server_pb.rb +++ b/lib/vagrant/protobufs/proto/ruby_vagrant/ruby-server_pb.rb @@ -5,6 +5,7 @@ require 'google/protobuf' require 'google/protobuf/empty_pb' require 'google/protobuf/any_pb' +require 'google/rpc/error_details_pb' require 'plugin_pb' Google::Protobuf::DescriptorPool.generated_pool.build do add_file("proto/ruby_vagrant/ruby-server.proto", :syntax => :proto3) do diff --git a/plugins/commands/serve/client.rb b/plugins/commands/serve/client.rb index d2387812f..45852a437 100644 --- a/plugins/commands/serve/client.rb +++ b/plugins/commands/serve/client.rb @@ -13,6 +13,7 @@ module VagrantPlugins autoload :PluginManager, Vagrant.source_root.join("plugins/commands/serve/client/plugin_manager").to_s autoload :Project, Vagrant.source_root.join("plugins/commands/serve/client/project").to_s autoload :Provider, Vagrant.source_root.join("plugins/commands/serve/client/provider").to_s + autoload :Provisioner, Vagrant.source_root.join("plugins/commands/serve/client/provisioner").to_s autoload :Push, Vagrant.source_root.join("plugins/commands/serve/client/push").to_s autoload :Target, Vagrant.source_root.join("plugins/commands/serve/client/target").to_s autoload :Terminal, Vagrant.source_root.join("plugins/commands/serve/client/terminal").to_s diff --git a/plugins/commands/serve/client/provisioner.rb b/plugins/commands/serve/client/provisioner.rb new file mode 100644 index 000000000..2516ac045 --- /dev/null +++ b/plugins/commands/serve/client/provisioner.rb @@ -0,0 +1,47 @@ +module VagrantPlugins + module CommandServe + class Client + class Provisioner < Client + prepend Util::ClientSetup + prepend Util::HasLogger + include Util::HasSeeds::Client + + def cleanup_func + spec = client.cleanup_spec(Empty.new) + cb = proc do |args| + client.cleanup(args) + end + [spec, cb] + end + + def cleanup(machine, config) + run_func(machine, config) + end + + def configure_func + spec = client.configure_spec(Empty.new) + cb = proc do |args| + client.configure(args) + end + [spec, cb] + end + + def configure(machine, config, root_config) + run_func(machine, config, root_config, {}) + end + + def provision_func + spec = client.provision_spec(Empty.new) + cb = proc do |args| + client.provision(args) + end + [spec, cb] + end + + def provision(machine, config) + run_func(machine, config) + end + end + end + end +end diff --git a/plugins/commands/serve/command.rb b/plugins/commands/serve/command.rb index aab60419b..eb69619cd 100644 --- a/plugins/commands/serve/command.rb +++ b/plugins/commands/serve/command.rb @@ -98,7 +98,7 @@ module VagrantPlugins [Service::InternalService, Service::ProviderService, Service::GuestService, Service::HostService, Service::CommandService, Service::SyncedFolderService, - Service::CommunicatorService, Service::PushService, + Service::CommunicatorService, Service::PushService, Service::ProvisionerService, Broker::Streamer].each do |service_klass| service = service_klass.new(broker: broker) s.handle(service) diff --git a/plugins/commands/serve/mappers.rb b/plugins/commands/serve/mappers.rb index be1fcc5c5..3f4c6fecd 100644 --- a/plugins/commands/serve/mappers.rb +++ b/plugins/commands/serve/mappers.rb @@ -363,6 +363,7 @@ require Vagrant.source_root.join("plugins/commands/serve/mappers/pathname.rb").t require Vagrant.source_root.join("plugins/commands/serve/mappers/plugin_manager.rb").to_s require Vagrant.source_root.join("plugins/commands/serve/mappers/project.rb").to_s require Vagrant.source_root.join("plugins/commands/serve/mappers/provider.rb").to_s +require Vagrant.source_root.join("plugins/commands/serve/mappers/provisioner.rb").to_s require Vagrant.source_root.join("plugins/commands/serve/mappers/push.rb").to_s require Vagrant.source_root.join("plugins/commands/serve/mappers/state_bag.rb").to_s require Vagrant.source_root.join("plugins/commands/serve/mappers/synced_folder.rb").to_s diff --git a/plugins/commands/serve/mappers/provisioner.rb b/plugins/commands/serve/mappers/provisioner.rb new file mode 100644 index 000000000..181491133 --- /dev/null +++ b/plugins/commands/serve/mappers/provisioner.rb @@ -0,0 +1,49 @@ +module VagrantPlugins + module CommandServe + class Mappers + # Build a guest client from a proto instance + class ProvisionerFromProto < Mapper + def initialize + inputs = [].tap do |i| + i << Input.new(type: SDK::Args::Provisioner) + i << Input.new(type: Broker) + end + super(inputs: inputs, output: Client::Provisioner, func: method(:converter)) + end + + def converter(proto, broker) + Client::Provisioner.load(proto, broker: broker) + end + end + + class GeneralConfigFromSpec < Mapper + def initialize + inputs = [].tap do |i| + i << Input.new(type: SDK::FuncSpec::Value) { |arg| + arg.type == "hashicorp.vagrant.sdk.Vagrantfile.GeneralConfig" && + !arg&.value&.value.nil? + } + end + super(inputs: inputs, output: SDK::Vagrantfile::GeneralConfig, func: method(:converter)) + end + + def converter(proto) + proto.value.unpack(SDK::Vagrantfile::GeneralConfig) + end + end + + class ConfigToProto < Mapper + def initialize + inputs = [].tap do |i| + i << Input.new(type: Vagrant::Plugin::V2::Config) + end + super(inputs: inputs, output: SDK::Vagrantfile::GeneralConfig, func: method(:converter)) + end + + def converter(config) + config.to_proto(config.class.to_s) + end + end + end + end +end diff --git a/plugins/commands/serve/service.rb b/plugins/commands/serve/service.rb index 8cfc13805..cd843d4b8 100644 --- a/plugins/commands/serve/service.rb +++ b/plugins/commands/serve/service.rb @@ -10,6 +10,7 @@ module VagrantPlugins autoload :HostService, Vagrant.source_root.join("plugins/commands/serve/service/host_service").to_s autoload :InternalService, Vagrant.source_root.join("plugins/commands/serve/service/internal_service").to_s autoload :ProviderService, Vagrant.source_root.join("plugins/commands/serve/service/provider_service").to_s + autoload :ProvisionerService, Vagrant.source_root.join("plugins/commands/serve/service/provisioner_service").to_s autoload :SyncedFolderService, Vagrant.source_root.join("plugins/commands/serve/service/synced_folder_service").to_s autoload :PushService, Vagrant.source_root.join("plugins/commands/serve/service/push_service").to_s diff --git a/plugins/commands/serve/service/provisioner_service.rb b/plugins/commands/serve/service/provisioner_service.rb new file mode 100644 index 000000000..582ffcd57 --- /dev/null +++ b/plugins/commands/serve/service/provisioner_service.rb @@ -0,0 +1,130 @@ +module VagrantPlugins + module CommandServe + module Service + class ProvisionerService < Hashicorp::Vagrant::Sdk::ProvisionerService::Service + prepend Util::HasMapper + prepend Util::HasBroker + prepend Util::HasLogger + + include Util::ServiceInfo + include Util::HasSeeds::Service + include Util::ExceptionTransformer + + def cleanup(req, ctx) + machine, plugin_config = _process_args(req) + + provisioner =_lookup_or_instantiate_provisioner(req, ctx, machine, plugin_config) + + provisioner.cleanup + end + + def cleanup_spec(*_) + SDK::FuncSpec.new( + name: "cleanup_spec", + args: [ + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Args.Target.Machine", + name: "", + ), + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Vagrantfile.GeneralConfig", + name: "", + ) + ], + result: [], + ) + end + + def configure(req, ctx) + machine, plugin_config = _process_args(req) + + provisioner =_lookup_or_instantiate_provisioner(req, ctx, machine, plugin_config) + + provisioner.configure(machine.config) + + Empty.new + end + + def configure_spec(*_) + SDK::FuncSpec.new( + name: "configure_spec", + args: [ + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Args.Target.Machine", + name: "", + ), + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Vagrantfile.GeneralConfig", + name: "", + ) + ], + result: [], + ) + end + + + def provision(req, ctx) + machine, plugin_config = _process_args(req) + + provisioner =_lookup_or_instantiate_provisioner(req, ctx, machine, plugin_config) + + provisioner.provision + + Empty.new + end + + def provision_spec(*_) + SDK::FuncSpec.new( + name: "provision_spec", + args: [ + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Args.Target.Machine", + name: "", + ), + SDK::FuncSpec::Value.new( + type: "hashicorp.vagrant.sdk.Vagrantfile.GeneralConfig", + name: "", + ) + ], + result: [], + ) + end + + # All of the methods here take the same args, so they have the same + # args processing which can be shared + # + # @param req the request + # @return [Array(Vagrant::Machine, Vagrant::Plugin::V2::Config)] + def _process_args(req) + machine, config = mapper.funcspec_map( + req, mapper, broker, expect: [ + Vagrant::Machine, SDK::Vagrantfile::GeneralConfig + ] + ) + config_klass = config.type.split('::').inject(Kernel) { |memo, obj| + memo.const_get(obj) + } + config_data = config.config.unpack(Google::Protobuf::Struct).to_h + plugin_config = config_klass.new + plugin_config.set_options(config_data) + return machine, plugin_config + end + + def _lookup_or_instantiate_provisioner(req, ctx, machine, plugin_config) + @_provisioner_cache ||= {} + key = ctx.metadata["plugin_name"] + if @_provisioner_cache[key] != nil + return @_provisioner_cache[key] + else + plugins = Vagrant.plugin("2").local_manager.provisioners + with_plugin(ctx, plugins, broker: broker) do |plugin| + provisioner = plugin.new(machine, plugin_config) + @_provisioner_cache[key] = provisioner + return provisioner + end + end + end + end + end + end +end diff --git a/plugins/commands/serve/util/service_info.rb b/plugins/commands/serve/util/service_info.rb index 22a43bf1a..45fe9c9e0 100644 --- a/plugins/commands/serve/util/service_info.rb +++ b/plugins/commands/serve/util/service_info.rb @@ -35,7 +35,7 @@ module VagrantPlugins def with_plugin(context, plugins, broker:, &block) with_info(context, broker: broker) do |info| plugin_name = info.plugin_name - plugin = plugins[plugin_name.to_s.to_sym].to_a.first + plugin = Array(plugins[plugin_name.to_s.to_sym]).first if !plugin raise NameError, "Failed to locate plugin named #{plugin_name}" end diff --git a/test/unit/vagrant/plugin/v2/config_test.rb b/test/unit/vagrant/plugin/v2/config_test.rb index 68b0d7ae8..66f996f20 100644 --- a/test/unit/vagrant/plugin/v2/config_test.rb +++ b/test/unit/vagrant/plugin/v2/config_test.rb @@ -72,5 +72,12 @@ describe Vagrant::Plugin::V2::Config do expect { subject.i_should_not_exist }. to raise_error(NoMethodError) end + + it "should survive being the last arg to a method that captures kwargs without a ruby conversion error" do + arg_capturer = lambda { |*args, **kwargs| } + expect { + arg_capturer.call(subject) + }.to_not raise_error + end end end