Allow go push plugins to use config from Vagrantfile

* Populate push configs when parsing the vagrantfile
* Allow untyped configs to be shipped over GRPC
* In our demo plugin, walk the vagrantfile and snag the config

Example Vagrantfile that works with the demo plugin:

```ruby
Vagrant.configure("2") do |config|
  config.push.define "myplugin" do |push|
    push.coolkey = "coolvalue"
    push.alist = ["so", "many", "items"]
    push.ahash = { "hashkey" => "hashvalue" }
  end
end

```
This commit is contained in:
Paul Hinze 2022-01-21 12:50:13 -06:00
parent a841da4fc6
commit 92c345b42d
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
7 changed files with 117 additions and 16 deletions

View File

@ -1,23 +1,84 @@
package push
import (
"encoding/json"
"github.com/golang/protobuf/ptypes"
"github.com/hashicorp/vagrant-plugin-sdk/component"
"github.com/hashicorp/vagrant-plugin-sdk/core"
"github.com/hashicorp/vagrant-plugin-sdk/proto/vagrant_plugin_sdk"
"github.com/hashicorp/vagrant-plugin-sdk/terminal"
"google.golang.org/protobuf/types/known/structpb"
)
// This is a push strategy that provides encouragement for the code you push
// Encouragement is a push strategy that provides encouragement for the code
// you push. Everybody could use some encouragement sometimes!
type Encouragement struct{}
func (e *Encouragement) PushFunc() interface{} {
return e.Push
}
// Push runs this is the first ever Golang push plugin!
func (e *Encouragement) Push(ui terminal.UI, proj core.Project) error {
ui.Output("You've invoked a push plugin written in Go! Great work!")
pushConfig, err := findPushConfig(proj, "myplugin")
if err != nil {
return err
}
if pushConfig != nil {
ui.Output("Look a this nice config you sent along too!")
ui.Output("We'll print it as JSON for fun:")
config, err := unpackConfig(pushConfig)
if err != nil {
return err
}
jsonConfig, err := json.MarshalIndent(config, " ", "\t")
if err != nil {
return err
}
ui.Output(" %s", jsonConfig)
}
return nil
}
// findPushConfig finds the relevant PushConfig for the name given.
//
// For now, there are no config related helpers, so each push plugin needs to
// walk its way down to its relevant config in the Vagrantfile.
func findPushConfig(proj core.Project, name string) (*vagrant_plugin_sdk.Vagrantfile_PushConfig, error) {
v, err := proj.Config()
if err != nil {
return nil, err
}
for _, p := range v.GetPushConfigs() {
if p.GetName() == name {
return p, nil
}
}
return nil, nil
}
// unpackConfig takes a PushConfig and unpack the underlying map of config
//
// For now, there are no config related helpers, so each push plugin needs to
// unpack from a generic struct into whatever types it might need. For this
// demo plugin we're just leaving it untyped.
func unpackConfig(pc *vagrant_plugin_sdk.Vagrantfile_PushConfig) (map[string]interface{}, error) {
gc := pc.GetConfig()
s := &structpb.Struct{}
err := ptypes.UnmarshalAny(gc.GetConfig(), s)
if err != nil {
return nil, err
}
return s.AsMap(), nil
}
var (
_ component.Push = (*Encouragement)(nil)
)

View File

@ -53,8 +53,8 @@ type Project struct {
ui terminal.UI
}
func (b *Project) Config() *vagrant_plugin_sdk.Vagrantfile_Vagrantfile {
return b.project.Configuration
func (b *Project) Config() (*vagrant_plugin_sdk.Vagrantfile_Vagrantfile, error) {
return b.project.Configuration, nil
}
// UI implements core.Project

View File

@ -7,11 +7,17 @@ module Vagrant
LOG = Log4r::Logger.new("vagrant::config::v2::dummy_config")
def method_missing(name, *args, &block)
# If a DummyConfig ends up as a last arg in a situation where it's
# being passed through a method with *kwargs, ruby 2.x will try to
# implicitly convert it into a hash. If it responds to to_hash but
# does not return an actual hash, Ruby gets mad.
if name == :to_hash
# There are a few scenarios where ruby will attempt to implicity
# coerce a given object into a certain type. DummyConfigs 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 DummyConfig back, Ruby will raise a
# TypeError. Doing the normal thing of raising NoMethodError allows
# DummyConfig 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
@ -45,6 +51,20 @@ module Vagrant
acc
end
end
# Converts this untyped config into a form suitable for passing over a
# GRPC connection. This is used for portions of config that might not
# have config classes implemented in Ruby.
#
# @param type [String] a name to put into the type field, e.g. plugin name
# @return [Hashicorp::Vagrant::Sdk::Vagrantfile::GeneralConfig]
def to_proto(type)
protoize = self.instance_variables_hash
protoize.delete_if{|k,v| k.start_with?("_") }
config_struct = Google::Protobuf::Struct.from_hash(protoize)
config_any = Google::Protobuf::Any.pack(config_struct)
Hashicorp::Vagrant::Sdk::Vagrantfile::GeneralConfig.new(type: type, config: config_any)
end
end
end
end

View File

@ -634,6 +634,9 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
add_message "hashicorp.vagrant.sdk.Project.CwdResponse" do
optional :path, :string, 1
end
add_message "hashicorp.vagrant.sdk.Project.ConfigResponse" do
optional :vagrantfile, :message, 1, "hashicorp.vagrant.sdk.Vagrantfile.Vagrantfile"
end
add_message "hashicorp.vagrant.sdk.Project.VagrantfileNameResponse" do
optional :name, :string, 1
end
@ -770,11 +773,16 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
optional :owner, :string, 9
optional :type, :string, 10
end
add_message "hashicorp.vagrant.sdk.Vagrantfile.PushConfig" do
optional :name, :string, 1
optional :config, :message, 2, "hashicorp.vagrant.sdk.Vagrantfile.GeneralConfig"
end
add_message "hashicorp.vagrant.sdk.Vagrantfile.Vagrantfile" do
optional :path, :string, 1
optional :raw, :string, 2
optional :current_version, :string, 3
repeated :machine_configs, :message, 4, "hashicorp.vagrant.sdk.Vagrantfile.MachineConfig"
repeated :push_configs, :message, 5, "hashicorp.vagrant.sdk.Vagrantfile.PushConfig"
end
add_message "hashicorp.vagrant.sdk.TargetIndex" do
end
@ -837,11 +845,6 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
optional :version, :string, 2
repeated :providers, :string, 3
end
add_message "hashicorp.vagrant.sdk.Push" do
end
add_message "hashicorp.vagrant.sdk.Push.PushResponse" do
optional :exit_code, :int32, 1
end
end
end
@ -1003,6 +1006,7 @@ module Hashicorp
Project::MachineNamesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.MachineNamesResponse").msgclass
Project::ActiveMachinesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.ActiveMachinesResponse").msgclass
Project::CwdResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.CwdResponse").msgclass
Project::ConfigResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.ConfigResponse").msgclass
Project::VagrantfileNameResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.VagrantfileNameResponse").msgclass
Project::VagrantfilePathResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.VagrantfilePathResponse").msgclass
Project::HomeResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Project.HomeResponse").msgclass
@ -1027,6 +1031,7 @@ module Hashicorp
Vagrantfile::Provider = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Vagrantfile.Provider").msgclass
Vagrantfile::Network = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Vagrantfile.Network").msgclass
Vagrantfile::SyncedFolder = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Vagrantfile.SyncedFolder").msgclass
Vagrantfile::PushConfig = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Vagrantfile.PushConfig").msgclass
Vagrantfile::Vagrantfile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Vagrantfile.Vagrantfile").msgclass
TargetIndex = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.TargetIndex").msgclass
TargetIndex::TargetIdentifier = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.TargetIndex.TargetIdentifier").msgclass
@ -1047,8 +1052,6 @@ module Hashicorp
BoxCollection::AllResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.BoxCollection.AllResponse").msgclass
BoxCollection::CleanRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.BoxCollection.CleanRequest").msgclass
BoxCollection::FindRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.BoxCollection.FindRequest").msgclass
Push = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Push").msgclass
Push::PushResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("hashicorp.vagrant.sdk.Push.PushResponse").msgclass
end
end
end

View File

@ -448,6 +448,7 @@ module Hashicorp
rpc :TargetIndex, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Args::TargetIndex
# rpc ActiveMachines(google.protobuf.Empty) returns (Project.ActiveMachinesResponse);
rpc :CWD, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Project::CwdResponse
rpc :Config, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Project::ConfigResponse
rpc :DataDir, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Args::DataDir::Project
rpc :VagrantfileName, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Project::VagrantfileNameResponse
rpc :VagrantfilePath, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Project::VagrantfilePathResponse
@ -566,7 +567,7 @@ module Hashicorp
self.service_name = 'hashicorp.vagrant.sdk.PushService'
rpc :PushSpec, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::FuncSpec
rpc :Push, ::Hashicorp::Vagrant::Sdk::FuncSpec::Args, ::Hashicorp::Vagrant::Sdk::Push::PushResponse
rpc :Push, ::Hashicorp::Vagrant::Sdk::FuncSpec::Args, ::Google::Protobuf::Empty
rpc :Seed, ::Hashicorp::Vagrant::Sdk::Args::Seeds, ::Google::Protobuf::Empty
rpc :Seeds, ::Google::Protobuf::Empty, ::Hashicorp::Vagrant::Sdk::Args::Seeds
end

View File

@ -70,11 +70,14 @@ module VagrantPlugins
)
end
push_configs = v.config.push.to_proto
vagrantfile = Hashicorp::Vagrant::Sdk::Vagrantfile::Vagrantfile.new(
path: path,
raw: raw,
current_version: Vagrant::Config::CURRENT_VERSION,
machine_configs: machine_configs,
push_configs: push_configs,
)
Hashicorp::Vagrant::ParseVagrantfileResponse.new(
vagrantfile: vagrantfile

View File

@ -147,6 +147,19 @@ module VagrantPlugins
raise "Must finalize first!" if !@__finalized
@__compiled_pushes.dup
end
def to_proto
raise "Must finalize first!" if !@__finalized
__compiled_pushes.map do |name, (strategy, config)|
@logger.info("making a proto! name: #{name.inspect}, strategy: #{strategy.inspect}, config: #{config.inspect}")
@logger.info("protofied config #{config.to_proto(strategy).inspect}")
Hashicorp::Vagrant::Sdk::Vagrantfile::PushConfig.new(
name: name,
config: config.to_proto(strategy)
)
end
end
end
end
end